diff --git a/.axiom/semantic_index/index.json b/.axiom/semantic_index/index.json index c2d8f1b2..94f7e45b 100644 --- a/.axiom/semantic_index/index.json +++ b/.axiom/semantic_index/index.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-05-26T06:29:57.952225530Z", + "generated_at": "2026-05-26T07:44:28.125947395Z", "root_path": "/home/busya/dev/ss-tools", - "contract_count": 3461, + "contract_count": 3195, "edge_count": 2140, - "file_count": 691, + "file_count": 690, "contracts": [ { "contract_id": "SrcRoot", @@ -3810,14 +3810,14 @@ { "source_id": "TestMigrationRoutes", "relation_type": "BINDS_TO", - "target_id": "EXT:path:backend.src.api.routes.migration", - "target_ref": "[EXT:path:backend.src.api.routes.migration]" + "target_id": "MigrationApi", + "target_ref": "[MigrationApi]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestMigrationRoutes [TYPE Module] [C:3] [SEMANTICS test, migration, api, route, handler]\n#\n# @BRIEF Unit tests for migration API route handlers.\n# @LAYER API\n# @RELATION BINDS_TO -> [EXT:path:backend.src.api.routes.migration]\n#\nfrom datetime import UTC, datetime\nfrom pathlib import Path\nimport pytest\nimport sys\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\n# Add backend directory to sys.path\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\nimport os\n\n# Force SQLite in-memory for all database connections BEFORE importing any application code\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"TASKS_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"AUTH_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"ENVIRONMENT\"] = \"testing\"\nfrom fastapi import HTTPException\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.models.mapping import Base, ResourceMapping, ResourceType\n\n# Patch the get_db dependency if `src.api.routes.migration` imports it\npatch(\"src.core.database.get_db\").start()\n# --- Fixtures ---\n@pytest.fixture\ndef db_session():\n \"\"\"In-memory SQLite session for testing.\"\"\"\n from sqlalchemy.pool import StaticPool\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n# #region _make_config_manager [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _make_config_manager(cron=\"0 2 * * *\"):\n \"\"\"Creates a mock config manager with a realistic AppConfig-like object.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = cron\n config = MagicMock()\n config.settings = settings\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.save_config = MagicMock()\n return cm\n# --- get_migration_settings tests ---\n# #endregion _make_config_manager\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_default_cron():\n \"\"\"Verify the settings endpoint returns the stored cron string.\"\"\"\n from src.api.routes.migration import get_migration_settings\n cm = _make_config_manager(cron=\"0 3 * * *\")\n # Call the handler directly, bypassing Depends\n result = await get_migration_settings(config_manager=cm, _=None)\n assert result == {\"cron\": \"0 3 * * *\"}\n cm.get_config.assert_called_once()\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_fallback_when_no_cron():\n \"\"\"When migration_sync_cron uses the default, should return '0 2 * * *'.\"\"\"\n from src.api.routes.migration import get_migration_settings\n # Use the default cron value (simulating a fresh config)\n cm = _make_config_manager()\n result = await get_migration_settings(config_manager=cm, _=None)\n assert result == {\"cron\": \"0 2 * * *\"}\n# --- update_migration_settings tests ---\n@pytest.mark.asyncio\nasync def test_update_migration_settings_saves_cron():\n \"\"\"Verify that a valid cron update saves to config.\"\"\"\n from src.api.routes.migration import update_migration_settings\n cm = _make_config_manager()\n result = await update_migration_settings(\n payload={\"cron\": \"0 4 * * *\"}, config_manager=cm, _=None\n )\n assert result[\"cron\"] == \"0 4 * * *\"\n assert result[\"status\"] == \"updated\"\n cm.save_config.assert_called_once()\n@pytest.mark.asyncio\nasync def test_update_migration_settings_rejects_missing_cron():\n \"\"\"Verify 400 error when 'cron' key is missing from payload.\"\"\"\n from src.api.routes.migration import update_migration_settings\n cm = _make_config_manager()\n with pytest.raises(HTTPException) as exc_info:\n await update_migration_settings(\n payload={\"interval\": \"daily\"}, config_manager=cm, _=None\n )\n assert exc_info.value.status_code == 400\n assert \"cron\" in exc_info.value.detail.lower()\n# --- get_resource_mappings tests ---\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_returns_formatted_list(db_session):\n \"\"\"Verify mappings are returned as formatted dicts with correct keys.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n # Populate test data\n m1 = ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"uuid-1\",\n remote_integer_id=\"42\",\n resource_name=\"Sales Chart\",\n last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=UTC),\n )\n db_session.add(m1)\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert len(result[\"items\"]) == 1\n assert result[\"items\"][0][\"environment_id\"] == \"prod\"\n assert result[\"items\"][0][\"resource_type\"] == \"chart\"\n assert result[\"items\"][0][\"uuid\"] == \"uuid-1\"\n assert result[\"items\"][0][\"remote_id\"] == \"42\"\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n assert result[\"items\"][0][\"last_synced_at\"] is not None\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_respects_pagination(db_session):\n \"\"\"Verify skip and limit parameters work correctly.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n for i in range(5):\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=f\"uuid-{i}\",\n remote_integer_id=str(i),\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=2,\n limit=2,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 5\n assert len(result[\"items\"]) == 2\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_search_by_name(db_session):\n \"\"\"Verify search filters by resource_name.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Sales Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Revenue Dashboard\",\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=\"sales\",\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_env(db_session):\n \"\"\"Verify env_id filter returns only matching environment.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n db_session.add(\n ResourceMapping(\n environment_id=\"ss1\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Chart A\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"ss2\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Chart B\",\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=\"ss2\",\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"environment_id\"] == \"ss2\"\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_type(db_session):\n \"\"\"Verify resource_type filter returns only matching type.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"My Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"My Dataset\",\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=\"dataset\",\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_type\"] == \"dataset\"\n# --- trigger_sync_now tests ---\n@pytest.fixture\n# #region _mock_env [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _mock_env():\n \"\"\"Creates a mock config environment object.\"\"\"\n env = MagicMock()\n env.id = \"test-env-1\"\n env.name = \"Test Env\"\n env.url = \"http://superset.test\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n return env\n# #endregion _mock_env\n# #region _make_sync_config_manager [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _make_sync_config_manager(environments):\n \"\"\"Creates a mock config manager with environments list.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = \"0 2 * * *\"\n config = MagicMock()\n config.settings = settings\n config.environments = environments\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.get_environments.return_value = environments\n return cm\n# #endregion _make_sync_config_manager\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_creates_env_row_and_syncs(db_session, _mock_env):\n \"\"\"Verify that trigger_sync_now creates an Environment row in DB before syncing,\n preventing FK constraint violations on resource_mappings inserts.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n cm = _make_sync_config_manager([_mock_env])\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_client_instance = MagicMock()\n MockClient.return_value = mock_client_instance\n mock_service_instance = MagicMock()\n MockService.return_value = mock_service_instance\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n # Environment row must exist in DB\n env_row = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").first()\n assert env_row is not None\n assert env_row.name == \"Test Env\"\n assert env_row.url == \"http://superset.test\"\n # Sync must have been called\n mock_service_instance.sync_environment.assert_called_once_with(\n \"test-env-1\", mock_client_instance\n )\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 0\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_rejects_empty_environments(db_session):\n \"\"\"Verify 400 error when no environments are configured.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n cm = _make_sync_config_manager([])\n with pytest.raises(HTTPException) as exc_info:\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n assert exc_info.value.status_code == 400\n assert \"No environments\" in exc_info.value.detail\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_handles_partial_failure(db_session, _mock_env):\n \"\"\"Verify that if sync_environment raises for one env, it's captured in failed list.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n env2 = MagicMock()\n env2.id = \"test-env-2\"\n env2.name = \"Failing Env\"\n env2.url = \"http://fail.test\"\n env2.username = \"admin\"\n env2.password = \"admin\"\n env2.verify_ssl = False\n env2.timeout = 30\n cm = _make_sync_config_manager([_mock_env, env2])\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_service_instance = MagicMock()\n mock_service_instance.sync_environment.side_effect = [\n None,\n RuntimeError(\"Connection refused\"),\n ]\n MockService.return_value = mock_service_instance\n MockClient.return_value = MagicMock()\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 1\n assert result[\"details\"][\"failed\"][0][\"env_id\"] == \"test-env-2\"\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):\n \"\"\"Verify that calling sync twice doesn't duplicate the Environment row.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n cm = _make_sync_config_manager([_mock_env])\n with (\n patch(\"src.api.routes.migration.SupersetClient\"),\n patch(\"src.api.routes.migration.IdMappingService\"),\n ):\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n env_count = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").count()\n assert env_count == 1\n# --- get_dashboards tests ---\n@pytest.mark.asyncio\nasync def test_get_dashboards_success(_mock_env):\n from src.api.routes.migration import get_dashboards\n cm = _make_sync_config_manager([_mock_env])\n with patch(\"src.api.routes.migration.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [{\"id\": 1, \"title\": \"Test\"}]\n MockClient.return_value = mock_client\n result = await get_dashboards(env_id=\"test-env-1\", config_manager=cm, _=None)\n assert len(result) == 1\n assert result[0][\"id\"] == 1\n@pytest.mark.asyncio\nasync def test_get_dashboards_invalid_env_raises_404(_mock_env):\n from src.api.routes.migration import get_dashboards\n cm = _make_sync_config_manager([_mock_env])\n with pytest.raises(HTTPException) as exc:\n await get_dashboards(env_id=\"wrong-env\", config_manager=cm, _=None)\n assert exc.value.status_code == 404\n# --- execute_migration tests ---\n@pytest.mark.asyncio\nasync def test_execute_migration_success(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n cm = _make_sync_config_manager([_mock_env, _mock_env]) # Need both source/target\n tm = MagicMock()\n tm.create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"test-env-1\", selected_ids=[1, 2]\n )\n result = await execute_migration(\n selection=selection, config_manager=cm, task_manager=tm, _=None\n )\n assert result[\"task_id\"] == \"task-123\"\n tm.create_task.assert_called_once()\n@pytest.mark.asyncio\nasync def test_execute_migration_invalid_env_raises_400(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n cm = _make_sync_config_manager([_mock_env])\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"non-existent\", selected_ids=[1]\n )\n with pytest.raises(HTTPException) as exc:\n await execute_migration(\n selection=selection, config_manager=cm, task_manager=MagicMock(), _=None\n )\n assert exc.value.status_code == 400\n@pytest.mark.asyncio\nasync def test_dry_run_migration_returns_diff_and_risk(db_session):\n # @TEST_EDGE missing_target_datasource -> validates high risk item generation\n # @TEST_EDGE breaking_reference -> validates high risk on missing dataset link\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n env_source = MagicMock()\n env_source.id = \"src\"\n env_source.name = \"Source\"\n env_source.url = \"http://source\"\n env_source.username = \"admin\"\n env_source.password = \"admin\"\n env_source.verify_ssl = False\n env_source.timeout = 30\n env_target = MagicMock()\n env_target.id = \"tgt\"\n env_target.name = \"Target\"\n env_target.url = \"http://target\"\n env_target.username = \"admin\"\n env_target.password = \"admin\"\n env_target.verify_ssl = False\n env_target.timeout = 30\n cm = _make_sync_config_manager([env_source, env_target])\n selection = DashboardSelection(\n selected_ids=[42],\n source_env_id=\"src\",\n target_env_id=\"tgt\",\n replace_db_config=False,\n fix_cross_filters=True,\n )\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.MigrationDryRunService\") as MockService,\n ):\n source_client = MagicMock()\n target_client = MagicMock()\n MockClient.side_effect = [source_client, target_client]\n service_instance = MagicMock()\n service_payload = {\n \"generated_at\": \"2026-02-27T00:00:00+00:00\",\n \"selection\": selection.model_dump(),\n \"selected_dashboard_titles\": [\"Sales\"],\n \"diff\": {\n \"dashboards\": {\n \"create\": [],\n \"update\": [{\"uuid\": \"dash-1\"}],\n \"delete\": [],\n },\n \"charts\": {\"create\": [{\"uuid\": \"chart-1\"}], \"update\": [], \"delete\": []},\n \"datasets\": {\n \"create\": [{\"uuid\": \"dataset-1\"}],\n \"update\": [],\n \"delete\": [],\n },\n },\n \"summary\": {\n \"dashboards\": {\"create\": 0, \"update\": 1, \"delete\": 0},\n \"charts\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"datasets\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"selected_dashboards\": 1,\n },\n \"risk\": {\n \"score\": 75,\n \"level\": \"high\",\n \"items\": [\n {\"code\": \"missing_datasource\"},\n {\"code\": \"breaking_reference\"},\n ],\n },\n }\n service_instance.run.return_value = service_payload\n MockService.return_value = service_instance\n result = await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n assert result[\"summary\"][\"dashboards\"][\"update\"] == 1\n assert result[\"summary\"][\"charts\"][\"create\"] == 1\n assert result[\"summary\"][\"datasets\"][\"create\"] == 1\n assert result[\"risk\"][\"score\"] > 0\n assert any(item[\"code\"] == \"missing_datasource\" for item in result[\"risk\"][\"items\"])\n assert any(item[\"code\"] == \"breaking_reference\" for item in result[\"risk\"][\"items\"])\n@pytest.mark.asyncio\nasync def test_dry_run_migration_rejects_same_environment(db_session):\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n env = MagicMock()\n env.id = \"same\"\n env.name = \"Same\"\n env.url = \"http://same\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n cm = _make_sync_config_manager([env])\n selection = DashboardSelection(\n selected_ids=[1], source_env_id=\"same\", target_env_id=\"same\"\n )\n with pytest.raises(HTTPException) as exc:\n await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n assert exc.value.status_code == 400\n# #endregion TestMigrationRoutes\n" + "body": "# #region TestMigrationRoutes [TYPE Module] [C:3] [SEMANTICS test, migration, api, route, handler]\n#\n# @BRIEF Unit tests for migration API route handlers.\n# @LAYER API\n# @RELATION BINDS_TO -> [MigrationApi]\n#\nfrom datetime import UTC, datetime\nfrom pathlib import Path\nimport pytest\nimport sys\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\n# Add backend directory to sys.path\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\nimport os\n\n# Force SQLite in-memory for all database connections BEFORE importing any application code\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"TASKS_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"AUTH_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"ENVIRONMENT\"] = \"testing\"\nfrom fastapi import HTTPException\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.models.mapping import Base, ResourceMapping, ResourceType\n\n# Patch the get_db dependency if `src.api.routes.migration` imports it\npatch(\"src.core.database.get_db\").start()\n# --- Fixtures ---\n@pytest.fixture\ndef db_session():\n \"\"\"In-memory SQLite session for testing.\"\"\"\n from sqlalchemy.pool import StaticPool\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n# #region _make_config_manager [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _make_config_manager(cron=\"0 2 * * *\"):\n \"\"\"Creates a mock config manager with a realistic AppConfig-like object.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = cron\n config = MagicMock()\n config.settings = settings\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.save_config = MagicMock()\n return cm\n# --- get_migration_settings tests ---\n# #endregion _make_config_manager\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_default_cron():\n \"\"\"Verify the settings endpoint returns the stored cron string.\"\"\"\n from src.api.routes.migration import get_migration_settings\n cm = _make_config_manager(cron=\"0 3 * * *\")\n # Call the handler directly, bypassing Depends\n result = await get_migration_settings(config_manager=cm, _=None)\n assert result == {\"cron\": \"0 3 * * *\"}\n cm.get_config.assert_called_once()\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_fallback_when_no_cron():\n \"\"\"When migration_sync_cron uses the default, should return '0 2 * * *'.\"\"\"\n from src.api.routes.migration import get_migration_settings\n # Use the default cron value (simulating a fresh config)\n cm = _make_config_manager()\n result = await get_migration_settings(config_manager=cm, _=None)\n assert result == {\"cron\": \"0 2 * * *\"}\n# --- update_migration_settings tests ---\n@pytest.mark.asyncio\nasync def test_update_migration_settings_saves_cron():\n \"\"\"Verify that a valid cron update saves to config.\"\"\"\n from src.api.routes.migration import update_migration_settings\n cm = _make_config_manager()\n result = await update_migration_settings(\n payload={\"cron\": \"0 4 * * *\"}, config_manager=cm, _=None\n )\n assert result[\"cron\"] == \"0 4 * * *\"\n assert result[\"status\"] == \"updated\"\n cm.save_config.assert_called_once()\n@pytest.mark.asyncio\nasync def test_update_migration_settings_rejects_missing_cron():\n \"\"\"Verify 400 error when 'cron' key is missing from payload.\"\"\"\n from src.api.routes.migration import update_migration_settings\n cm = _make_config_manager()\n with pytest.raises(HTTPException) as exc_info:\n await update_migration_settings(\n payload={\"interval\": \"daily\"}, config_manager=cm, _=None\n )\n assert exc_info.value.status_code == 400\n assert \"cron\" in exc_info.value.detail.lower()\n# --- get_resource_mappings tests ---\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_returns_formatted_list(db_session):\n \"\"\"Verify mappings are returned as formatted dicts with correct keys.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n # Populate test data\n m1 = ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"uuid-1\",\n remote_integer_id=\"42\",\n resource_name=\"Sales Chart\",\n last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=UTC),\n )\n db_session.add(m1)\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert len(result[\"items\"]) == 1\n assert result[\"items\"][0][\"environment_id\"] == \"prod\"\n assert result[\"items\"][0][\"resource_type\"] == \"chart\"\n assert result[\"items\"][0][\"uuid\"] == \"uuid-1\"\n assert result[\"items\"][0][\"remote_id\"] == \"42\"\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n assert result[\"items\"][0][\"last_synced_at\"] is not None\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_respects_pagination(db_session):\n \"\"\"Verify skip and limit parameters work correctly.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n for i in range(5):\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=f\"uuid-{i}\",\n remote_integer_id=str(i),\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=2,\n limit=2,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 5\n assert len(result[\"items\"]) == 2\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_search_by_name(db_session):\n \"\"\"Verify search filters by resource_name.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Sales Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Revenue Dashboard\",\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=\"sales\",\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_env(db_session):\n \"\"\"Verify env_id filter returns only matching environment.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n db_session.add(\n ResourceMapping(\n environment_id=\"ss1\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Chart A\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"ss2\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Chart B\",\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=\"ss2\",\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"environment_id\"] == \"ss2\"\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_type(db_session):\n \"\"\"Verify resource_type filter returns only matching type.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"My Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"My Dataset\",\n )\n )\n db_session.commit()\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=\"dataset\",\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_type\"] == \"dataset\"\n# --- trigger_sync_now tests ---\n@pytest.fixture\n# #region _mock_env [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _mock_env():\n \"\"\"Creates a mock config environment object.\"\"\"\n env = MagicMock()\n env.id = \"test-env-1\"\n env.name = \"Test Env\"\n env.url = \"http://superset.test\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n return env\n# #endregion _mock_env\n# #region _make_sync_config_manager [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _make_sync_config_manager(environments):\n \"\"\"Creates a mock config manager with environments list.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = \"0 2 * * *\"\n config = MagicMock()\n config.settings = settings\n config.environments = environments\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.get_environments.return_value = environments\n return cm\n# #endregion _make_sync_config_manager\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_creates_env_row_and_syncs(db_session, _mock_env):\n \"\"\"Verify that trigger_sync_now creates an Environment row in DB before syncing,\n preventing FK constraint violations on resource_mappings inserts.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n cm = _make_sync_config_manager([_mock_env])\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_client_instance = MagicMock()\n MockClient.return_value = mock_client_instance\n mock_service_instance = MagicMock()\n MockService.return_value = mock_service_instance\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n # Environment row must exist in DB\n env_row = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").first()\n assert env_row is not None\n assert env_row.name == \"Test Env\"\n assert env_row.url == \"http://superset.test\"\n # Sync must have been called\n mock_service_instance.sync_environment.assert_called_once_with(\n \"test-env-1\", mock_client_instance\n )\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 0\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_rejects_empty_environments(db_session):\n \"\"\"Verify 400 error when no environments are configured.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n cm = _make_sync_config_manager([])\n with pytest.raises(HTTPException) as exc_info:\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n assert exc_info.value.status_code == 400\n assert \"No environments\" in exc_info.value.detail\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_handles_partial_failure(db_session, _mock_env):\n \"\"\"Verify that if sync_environment raises for one env, it's captured in failed list.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n env2 = MagicMock()\n env2.id = \"test-env-2\"\n env2.name = \"Failing Env\"\n env2.url = \"http://fail.test\"\n env2.username = \"admin\"\n env2.password = \"admin\"\n env2.verify_ssl = False\n env2.timeout = 30\n cm = _make_sync_config_manager([_mock_env, env2])\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_service_instance = MagicMock()\n mock_service_instance.sync_environment.side_effect = [\n None,\n RuntimeError(\"Connection refused\"),\n ]\n MockService.return_value = mock_service_instance\n MockClient.return_value = MagicMock()\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 1\n assert result[\"details\"][\"failed\"][0][\"env_id\"] == \"test-env-2\"\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):\n \"\"\"Verify that calling sync twice doesn't duplicate the Environment row.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n cm = _make_sync_config_manager([_mock_env])\n with (\n patch(\"src.api.routes.migration.SupersetClient\"),\n patch(\"src.api.routes.migration.IdMappingService\"),\n ):\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n env_count = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").count()\n assert env_count == 1\n# --- get_dashboards tests ---\n@pytest.mark.asyncio\nasync def test_get_dashboards_success(_mock_env):\n from src.api.routes.migration import get_dashboards\n cm = _make_sync_config_manager([_mock_env])\n with patch(\"src.api.routes.migration.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [{\"id\": 1, \"title\": \"Test\"}]\n MockClient.return_value = mock_client\n result = await get_dashboards(env_id=\"test-env-1\", config_manager=cm, _=None)\n assert len(result) == 1\n assert result[0][\"id\"] == 1\n@pytest.mark.asyncio\nasync def test_get_dashboards_invalid_env_raises_404(_mock_env):\n from src.api.routes.migration import get_dashboards\n cm = _make_sync_config_manager([_mock_env])\n with pytest.raises(HTTPException) as exc:\n await get_dashboards(env_id=\"wrong-env\", config_manager=cm, _=None)\n assert exc.value.status_code == 404\n# --- execute_migration tests ---\n@pytest.mark.asyncio\nasync def test_execute_migration_success(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n cm = _make_sync_config_manager([_mock_env, _mock_env]) # Need both source/target\n tm = MagicMock()\n tm.create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"test-env-1\", selected_ids=[1, 2]\n )\n result = await execute_migration(\n selection=selection, config_manager=cm, task_manager=tm, _=None\n )\n assert result[\"task_id\"] == \"task-123\"\n tm.create_task.assert_called_once()\n@pytest.mark.asyncio\nasync def test_execute_migration_invalid_env_raises_400(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n cm = _make_sync_config_manager([_mock_env])\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"non-existent\", selected_ids=[1]\n )\n with pytest.raises(HTTPException) as exc:\n await execute_migration(\n selection=selection, config_manager=cm, task_manager=MagicMock(), _=None\n )\n assert exc.value.status_code == 400\n@pytest.mark.asyncio\nasync def test_dry_run_migration_returns_diff_and_risk(db_session):\n # @TEST_EDGE missing_target_datasource -> validates high risk item generation\n # @TEST_EDGE breaking_reference -> validates high risk on missing dataset link\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n env_source = MagicMock()\n env_source.id = \"src\"\n env_source.name = \"Source\"\n env_source.url = \"http://source\"\n env_source.username = \"admin\"\n env_source.password = \"admin\"\n env_source.verify_ssl = False\n env_source.timeout = 30\n env_target = MagicMock()\n env_target.id = \"tgt\"\n env_target.name = \"Target\"\n env_target.url = \"http://target\"\n env_target.username = \"admin\"\n env_target.password = \"admin\"\n env_target.verify_ssl = False\n env_target.timeout = 30\n cm = _make_sync_config_manager([env_source, env_target])\n selection = DashboardSelection(\n selected_ids=[42],\n source_env_id=\"src\",\n target_env_id=\"tgt\",\n replace_db_config=False,\n fix_cross_filters=True,\n )\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.MigrationDryRunService\") as MockService,\n ):\n source_client = MagicMock()\n target_client = MagicMock()\n MockClient.side_effect = [source_client, target_client]\n service_instance = MagicMock()\n service_payload = {\n \"generated_at\": \"2026-02-27T00:00:00+00:00\",\n \"selection\": selection.model_dump(),\n \"selected_dashboard_titles\": [\"Sales\"],\n \"diff\": {\n \"dashboards\": {\n \"create\": [],\n \"update\": [{\"uuid\": \"dash-1\"}],\n \"delete\": [],\n },\n \"charts\": {\"create\": [{\"uuid\": \"chart-1\"}], \"update\": [], \"delete\": []},\n \"datasets\": {\n \"create\": [{\"uuid\": \"dataset-1\"}],\n \"update\": [],\n \"delete\": [],\n },\n },\n \"summary\": {\n \"dashboards\": {\"create\": 0, \"update\": 1, \"delete\": 0},\n \"charts\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"datasets\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"selected_dashboards\": 1,\n },\n \"risk\": {\n \"score\": 75,\n \"level\": \"high\",\n \"items\": [\n {\"code\": \"missing_datasource\"},\n {\"code\": \"breaking_reference\"},\n ],\n },\n }\n service_instance.run.return_value = service_payload\n MockService.return_value = service_instance\n result = await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n assert result[\"summary\"][\"dashboards\"][\"update\"] == 1\n assert result[\"summary\"][\"charts\"][\"create\"] == 1\n assert result[\"summary\"][\"datasets\"][\"create\"] == 1\n assert result[\"risk\"][\"score\"] > 0\n assert any(item[\"code\"] == \"missing_datasource\" for item in result[\"risk\"][\"items\"])\n assert any(item[\"code\"] == \"breaking_reference\" for item in result[\"risk\"][\"items\"])\n@pytest.mark.asyncio\nasync def test_dry_run_migration_rejects_same_environment(db_session):\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n env = MagicMock()\n env.id = \"same\"\n env.name = \"Same\"\n env.url = \"http://same\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n cm = _make_sync_config_manager([env])\n selection = DashboardSelection(\n selected_ids=[1], source_env_id=\"same\", target_env_id=\"same\"\n )\n with pytest.raises(HTTPException) as exc:\n await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n assert exc.value.status_code == 400\n# #endregion TestMigrationRoutes\n" }, { "contract_id": "_mock_env", @@ -4968,14 +4968,14 @@ { "source_id": "AdminApiKeyRoutes", "relation_type": "DEPENDS_ON", - "target_id": "EXT:code:has_permission(\"admin:settings\", \"WRITE\")", - "target_ref": "[EXT:code:has_permission(\"admin:settings\", \"WRITE\")]" + "target_id": "EXT:code:has_permission_admin_settings_WRITE", + "target_ref": "[EXT:code:has_permission_admin_settings_WRITE]" } ], "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, 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" }, { "contract_id": "ApiKeyCreateRequest", @@ -5711,7 +5711,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 -> [EXT:internal: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 -> [EXT:internal: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 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" }, { "contract_id": "_append_history", @@ -5734,14 +5734,14 @@ { "source_id": "_append_history", "relation_type": "BINDS_TO", - "target_id": "EXT:internal:CONVERSATIONS", - "target_ref": "[EXT:internal:CONVERSATIONS]" + "target_id": "CONVERSATIONS", + "target_ref": "[CONVERSATIONS]" } ], "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 -> [EXT:internal: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.utcnow(),\n }\n )\n\n\n# #endregion _append_history\n" }, { "contract_id": "_persist_message", @@ -5794,14 +5794,14 @@ { "source_id": "_audit", "relation_type": "BINDS_TO", - "target_id": "EXT:internal:ASSISTANT_AUDIT", - "target_ref": "[EXT:internal:ASSISTANT_AUDIT]" + "target_id": "ASSISTANT_AUDIT", + "target_ref": "[ASSISTANT_AUDIT]" } ], "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 -> [EXT:internal: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.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n" }, { "contract_id": "_persist_audit", @@ -6591,7 +6591,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_schemas.py", "start_line": 1, - "end_line": 143, + "end_line": 150, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6618,7 +6618,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantSchemas [C:2] [TYPE Module] [SEMANTICS assistant, pydantic, schema, store, permission]\n# @BRIEF Pydantic models, in-memory stores, and permission mappings for the assistant API.\n# @LAYER API\n# @RELATION CALLED_BY -> [AssistantHistory]\n# @RELATION CALLED_BY -> [AssistantHistory]\n# @INVARIANT In-memory stores are module-level singletons shared across the assistant package.\n# @RATIONALE In-memory stores documented with NOTE about restart loss. ASSISTANT_ARCHIVE_AFTER_DAYS and ASSISTANT_MESSAGE_TTL_DAYS kept as module-level constants with TODO for config migration — Pydantic schemas module should not depend on ConfigManager for architectural purity.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region AssistantMessageRequest [C:1] [TYPE Class]\n# @BRIEF Input payload for assistant message endpoint.\n# @DATA_CONTRACT Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]\n# @RELATION CALLED_BY -> [send_message]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE message length is within accepted bounds.\n# @POST Request object provides message text and optional conversation binding.\n# @INVARIANT message is always non-empty and no longer than 4000 characters.\nclass AssistantMessageRequest(BaseModel):\n conversation_id: str | None = None\n message: str = Field(..., min_length=1, max_length=4000)\n dataset_review_session_id: str | None = None\n\n\n# #endregion AssistantMessageRequest\n\n\n# #region AssistantAction [C:1] [TYPE Class]\n# @BRIEF UI action descriptor returned with assistant responses.\n# @DATA_CONTRACT Input[type:str, label:str, target?:str] -> Output[AssistantAction]\n# @RELATION CALLED_BY -> [AssistantMessageResponse]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE type and label are provided by orchestration logic.\n# @POST Action can be rendered as button on frontend.\n# @INVARIANT type and label are required for every UI action.\nclass AssistantAction(BaseModel):\n type: str\n label: str\n target: str | None = None\n\n\n# #endregion AssistantAction\n\n\n# #region AssistantMessageResponse [C:1] [TYPE Class]\n# @BRIEF Output payload contract for assistant interaction endpoints.\n# @DATA_CONTRACT Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]\n# @RELATION CALLED_BY -> [send_message]\n# @RELATION CALLED_BY -> [confirm_operation]\n# @RELATION CALLED_BY -> [cancel_operation]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE Response includes deterministic state and text.\n# @POST Payload may include task_id/confirmation_id/actions for UI follow-up.\n# @INVARIANT created_at and state are always present in endpoint responses.\nclass AssistantMessageResponse(BaseModel):\n conversation_id: str\n response_id: str\n state: str\n text: str\n intent: dict[str, Any] | None = None\n confirmation_id: str | None = None\n task_id: str | None = None\n actions: list[AssistantAction] = Field(default_factory=list)\n created_at: datetime\n\n\n# #endregion AssistantMessageResponse\n\n\n# #region ConfirmationRecord [C:1] [TYPE Class]\n# @BRIEF In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION CALLED_BY -> [send_message]\n# @RELATION CALLED_BY -> [confirm_operation]\n# @RELATION CALLED_BY -> [cancel_operation]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE intent/dispatch/user_id are populated at confirmation request time.\n# @POST Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: dict[str, Any]\n dispatch: dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# #endregion ConfirmationRecord\n\n\n# --- In-memory stores ---\n\nCONVERSATIONS: dict[tuple[str, str], list[dict[str, Any]]] = {}\nUSER_ACTIVE_CONVERSATION: dict[str, str] = {}\nCONFIRMATIONS: dict[str, ConfirmationRecord] = {}\nASSISTANT_AUDIT: dict[str, list[dict[str, Any]]] = {}\nASSISTANT_ARCHIVE_AFTER_DAYS = 14\nASSISTANT_MESSAGE_TTL_DAYS = 90\n\n# Operations that are read-only and do not require confirmation.\n_SAFE_OPS = {\n \"show_capabilities\",\n \"get_task_status\",\n \"get_health_summary\",\n \"dataset_review_answer_context\",\n}\n\n_DATASET_REVIEW_OPS = {\n \"dataset_review_approve_mappings\",\n \"dataset_review_set_field_semantics\",\n \"dataset_review_generate_sql_preview\",\n}\n\nINTENT_PERMISSION_CHECKS: dict[str, list[tuple[str, str]]] = {\n \"get_task_status\": [(\"tasks\", \"READ\")],\n \"create_branch\": [(\"plugin:git\", \"EXECUTE\")],\n \"commit_changes\": [(\"plugin:git\", \"EXECUTE\")],\n \"deploy_dashboard\": [(\"plugin:git\", \"EXECUTE\")],\n \"execute_migration\": [\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:superset-migration\", \"EXECUTE\"),\n ],\n \"run_backup\": [(\"plugin:superset-backup\", \"EXECUTE\"), (\"plugin:backup\", \"EXECUTE\")],\n \"run_llm_validation\": [(\"plugin:llm_dashboard_validation\", \"EXECUTE\")],\n \"run_llm_documentation\": [(\"plugin:llm_documentation\", \"EXECUTE\")],\n \"get_health_summary\": [(\"plugin:migration\", \"READ\")],\n \"dataset_review_answer_context\": [(\"dataset:session\", \"READ\")],\n \"dataset_review_approve_mappings\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_set_field_semantics\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_generate_sql_preview\": [(\"dataset:session\", \"MANAGE\")],\n}\n\n\n# #endregion AssistantSchemas\n" + "body": "# #region AssistantSchemas [C:2] [TYPE Module] [SEMANTICS assistant, pydantic, schema, store, permission]\n# @BRIEF Pydantic models, in-memory stores, and permission mappings for the assistant API.\n# @LAYER API\n# @RELATION CALLED_BY -> [AssistantHistory]\n# @RELATION CALLED_BY -> [AssistantHistory]\n# @INVARIANT In-memory stores are module-level singletons shared across the assistant package.\n# @RATIONALE In-memory stores documented with NOTE about restart loss. ASSISTANT_ARCHIVE_AFTER_DAYS and ASSISTANT_MESSAGE_TTL_DAYS kept as module-level constants with TODO for config migration — Pydantic schemas module should not depend on ConfigManager for architectural purity.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region AssistantMessageRequest [C:1] [TYPE Class]\n# @BRIEF Input payload for assistant message endpoint.\n# @DATA_CONTRACT Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]\n# @RELATION CALLED_BY -> [send_message]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE message length is within accepted bounds.\n# @POST Request object provides message text and optional conversation binding.\n# @INVARIANT message is always non-empty and no longer than 4000 characters.\nclass AssistantMessageRequest(BaseModel):\n conversation_id: str | None = None\n message: str = Field(..., min_length=1, max_length=4000)\n dataset_review_session_id: str | None = None\n\n\n# #endregion AssistantMessageRequest\n\n\n# #region AssistantAction [C:1] [TYPE Class]\n# @BRIEF UI action descriptor returned with assistant responses.\n# @DATA_CONTRACT Input[type:str, label:str, target?:str] -> Output[AssistantAction]\n# @RELATION CALLED_BY -> [AssistantMessageResponse]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE type and label are provided by orchestration logic.\n# @POST Action can be rendered as button on frontend.\n# @INVARIANT type and label are required for every UI action.\nclass AssistantAction(BaseModel):\n type: str\n label: str\n target: str | None = None\n\n\n# #endregion AssistantAction\n\n\n# #region AssistantMessageResponse [C:1] [TYPE Class]\n# @BRIEF Output payload contract for assistant interaction endpoints.\n# @DATA_CONTRACT Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]\n# @RELATION CALLED_BY -> [send_message]\n# @RELATION CALLED_BY -> [confirm_operation]\n# @RELATION CALLED_BY -> [cancel_operation]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE Response includes deterministic state and text.\n# @POST Payload may include task_id/confirmation_id/actions for UI follow-up.\n# @INVARIANT created_at and state are always present in endpoint responses.\nclass AssistantMessageResponse(BaseModel):\n conversation_id: str\n response_id: str\n state: str\n text: str\n intent: dict[str, Any] | None = None\n confirmation_id: str | None = None\n task_id: str | None = None\n actions: list[AssistantAction] = Field(default_factory=list)\n created_at: datetime\n\n\n# #endregion AssistantMessageResponse\n\n\n# #region ConfirmationRecord [C:1] [TYPE Class]\n# @BRIEF In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION CALLED_BY -> [send_message]\n# @RELATION CALLED_BY -> [confirm_operation]\n# @RELATION CALLED_BY -> [cancel_operation]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE intent/dispatch/user_id are populated at confirmation request time.\n# @POST Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: dict[str, Any]\n dispatch: dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# #endregion ConfirmationRecord\n\n\n# --- In-memory stores ---\n\n# #region CONVERSATIONS [C:1] [TYPE Constant]\n# @BRIEF In-memory conversation history store: {(user_id, conversation_id) -> [message_records]}\n# #endregion CONVERSATIONS\nCONVERSATIONS: dict[tuple[str, str], list[dict[str, Any]]] = {}\n\n# #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\nUSER_ACTIVE_CONVERSATION: dict[str, str] = {}\nCONFIRMATIONS: dict[str, ConfirmationRecord] = {}\nASSISTANT_AUDIT: dict[str, list[dict[str, Any]]] = {}\nASSISTANT_ARCHIVE_AFTER_DAYS = 14\nASSISTANT_MESSAGE_TTL_DAYS = 90\n\n# Operations that are read-only and do not require confirmation.\n_SAFE_OPS = {\n \"show_capabilities\",\n \"get_task_status\",\n \"get_health_summary\",\n \"dataset_review_answer_context\",\n}\n\n_DATASET_REVIEW_OPS = {\n \"dataset_review_approve_mappings\",\n \"dataset_review_set_field_semantics\",\n \"dataset_review_generate_sql_preview\",\n}\n\nINTENT_PERMISSION_CHECKS: dict[str, list[tuple[str, str]]] = {\n \"get_task_status\": [(\"tasks\", \"READ\")],\n \"create_branch\": [(\"plugin:git\", \"EXECUTE\")],\n \"commit_changes\": [(\"plugin:git\", \"EXECUTE\")],\n \"deploy_dashboard\": [(\"plugin:git\", \"EXECUTE\")],\n \"execute_migration\": [\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:superset-migration\", \"EXECUTE\"),\n ],\n \"run_backup\": [(\"plugin:superset-backup\", \"EXECUTE\"), (\"plugin:backup\", \"EXECUTE\")],\n \"run_llm_validation\": [(\"plugin:llm_dashboard_validation\", \"EXECUTE\")],\n \"run_llm_documentation\": [(\"plugin:llm_documentation\", \"EXECUTE\")],\n \"get_health_summary\": [(\"plugin:migration\", \"READ\")],\n \"dataset_review_answer_context\": [(\"dataset:session\", \"READ\")],\n \"dataset_review_approve_mappings\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_set_field_semantics\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_generate_sql_preview\": [(\"dataset:session\", \"MANAGE\")],\n}\n\n\n# #endregion AssistantSchemas\n" }, { "contract_id": "AssistantMessageRequest", @@ -6764,6 +6764,42 @@ "tier_source": "AutoCalculated", "body": "# #region ConfirmationRecord [C:1] [TYPE Class]\n# @BRIEF In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION CALLED_BY -> [send_message]\n# @RELATION CALLED_BY -> [confirm_operation]\n# @RELATION CALLED_BY -> [cancel_operation]\n# @SIDE_EFFECT None (schema declaration only).\n# @PRE intent/dispatch/user_id are populated at confirmation request time.\n# @POST Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: dict[str, Any]\n dispatch: dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# #endregion ConfirmationRecord\n" }, + { + "contract_id": "CONVERSATIONS", + "contract_type": "Constant", + "file_path": "backend/src/api/routes/assistant/_schemas.py", + "start_line": 102, + "end_line": 104, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "In-memory conversation history store: {(user_id, conversation_id) -> [message_records]}", + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region CONVERSATIONS [C:1] [TYPE Constant]\n# @BRIEF In-memory conversation history store: {(user_id, conversation_id) -> [message_records]}\n# #endregion CONVERSATIONS\n" + }, + { + "contract_id": "ASSISTANT_AUDIT", + "contract_type": "Constant", + "file_path": "backend/src/api/routes/assistant/_schemas.py", + "start_line": 107, + "end_line": 109, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "In-memory assistant audit trail: {user_id -> [audit_records]}. Volatile — lost on restart.", + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "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": "CleanReleaseApi", "contract_type": "Module", @@ -7397,8 +7433,8 @@ { "source_id": "DashboardActionRoutes", "relation_type": "DEPENDS_ON", - "target_id": "[EXT:frontend:DashboardRouter]", - "target_ref": "[[EXT:frontend:DashboardRouter]]" + "target_id": "EXT:frontend:DashboardRouter]", + "target_ref": "[EXT:frontend:DashboardRouter]]" }, { "source_id": "DashboardActionRoutes", @@ -7410,7 +7446,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DashboardActionRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, backup]\n# @BRIEF Dashboard action route handlers — migrate, backup.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n has_permission,\n)\n\nfrom ._router import router\nfrom ._schemas import (\n BackupRequest,\n MigrateRequest,\n TaskResponse,\n)\n\n\n# #region migrate_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk migration of dashboards from source to target environment\n# @PRE User has permission plugin:migration:execute\n# @PRE source_env_id and target_env_id are valid environment IDs\n# @PRE dashboard_ids is a non-empty list\n# @POST Returns task_id for tracking migration progress\n# @POST Task is created and queued for execution\n# @RELATION DISPATCHES -> [EXT:method:MigrationPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/migrate\", response_model=TaskResponse)\nasync def migrate_dashboards(\n request: MigrateRequest,\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(\n \"migrate_dashboards\",\n f\"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n source_env = next(\n (e for e in environments if e.id == request.source_env_id), None\n )\n target_env = next(\n (e for e in environments if e.id == request.target_env_id), None\n )\n\n if not source_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.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\"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n task_params = {\n \"source_env_id\": request.source_env_id,\n \"target_env_id\": request.target_env_id,\n \"selected_ids\": request.dashboard_ids,\n \"replace_db_config\": request.replace_db_config,\n \"db_mappings\": request.db_mappings or {},\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-migration\", params=task_params\n )\n\n logger.info(\n f\"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create migration task: {e!s}\"\n )\n\n\n# #endregion migrate_dashboards\n\n\n# #region backup_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk backup of dashboards with optional cron schedule\n# @PRE User has permission plugin:backup:execute\n# @PRE env_id is a valid environment ID\n# @PRE dashboard_ids is a non-empty list\n# @POST Returns task_id for tracking backup progress\n# @POST Task is created and queued for execution\n# @POST If schedule is provided, a scheduled task is created\n# @RELATION DISPATCHES -> [EXT:method:BackupPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/backup\", response_model=TaskResponse)\nasync def backup_dashboards(\n request: BackupRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:backup\", \"EXECUTE\")),\n):\n with belief_scope(\n \"backup_dashboards\",\n f\"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[backup_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\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(\n f\"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n task_params = {\n \"env\": request.env_id,\n \"dashboards\": request.dashboard_ids,\n \"schedule\": request.schedule,\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=task_params\n )\n\n logger.info(\n f\"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create backup task: {e!s}\"\n )\n\n\n# #endregion backup_dashboards\n# #endregion DashboardActionRoutes\n" + "body": "# #region DashboardActionRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, backup]\n# @BRIEF Dashboard action route handlers — migrate, backup.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:frontend:DashboardRouter]]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n has_permission,\n)\n\nfrom ._router import router\nfrom ._schemas import (\n BackupRequest,\n MigrateRequest,\n TaskResponse,\n)\n\n\n# #region migrate_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk migration of dashboards from source to target environment\n# @PRE User has permission plugin:migration:execute\n# @PRE source_env_id and target_env_id are valid environment IDs\n# @PRE dashboard_ids is a non-empty list\n# @POST Returns task_id for tracking migration progress\n# @POST Task is created and queued for execution\n# @RELATION DISPATCHES -> [EXT:method:MigrationPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/migrate\", response_model=TaskResponse)\nasync def migrate_dashboards(\n request: MigrateRequest,\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(\n \"migrate_dashboards\",\n f\"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n source_env = next(\n (e for e in environments if e.id == request.source_env_id), None\n )\n target_env = next(\n (e for e in environments if e.id == request.target_env_id), None\n )\n\n if not source_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.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\"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n task_params = {\n \"source_env_id\": request.source_env_id,\n \"target_env_id\": request.target_env_id,\n \"selected_ids\": request.dashboard_ids,\n \"replace_db_config\": request.replace_db_config,\n \"db_mappings\": request.db_mappings or {},\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-migration\", params=task_params\n )\n\n logger.info(\n f\"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create migration task: {e!s}\"\n )\n\n\n# #endregion migrate_dashboards\n\n\n# #region backup_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk backup of dashboards with optional cron schedule\n# @PRE User has permission plugin:backup:execute\n# @PRE env_id is a valid environment ID\n# @PRE dashboard_ids is a non-empty list\n# @POST Returns task_id for tracking backup progress\n# @POST Task is created and queued for execution\n# @POST If schedule is provided, a scheduled task is created\n# @RELATION DISPATCHES -> [EXT:method:BackupPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/backup\", response_model=TaskResponse)\nasync def backup_dashboards(\n request: BackupRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:backup\", \"EXECUTE\")),\n):\n with belief_scope(\n \"backup_dashboards\",\n f\"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[backup_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\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(\n f\"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n task_params = {\n \"env\": request.env_id,\n \"dashboards\": request.dashboard_ids,\n \"schedule\": request.schedule,\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=task_params\n )\n\n logger.info(\n f\"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create backup task: {e!s}\"\n )\n\n\n# #endregion backup_dashboards\n# #endregion DashboardActionRoutes\n" }, { "contract_id": "migrate_dashboards", @@ -7495,8 +7531,8 @@ { "source_id": "DashboardDetailRoutes", "relation_type": "DEPENDS_ON", - "target_id": "[EXT:frontend:DashboardRouter]", - "target_ref": "[[EXT:frontend:DashboardRouter]]" + "target_id": "EXT:frontend:DashboardRouter]", + "target_ref": "[EXT:frontend:DashboardRouter]]" }, { "source_id": "DashboardDetailRoutes", @@ -7520,7 +7556,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 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" }, { "contract_id": "get_database_mappings", @@ -7792,8 +7828,8 @@ { "source_id": "DashboardListingRoutes", "relation_type": "DEPENDS_ON", - "target_id": "[EXT:frontend:DashboardRouter]", - "target_ref": "[[EXT:frontend:DashboardRouter]]" + "target_id": "EXT:frontend:DashboardRouter]", + "target_ref": "[EXT:frontend:DashboardRouter]]" }, { "source_id": "DashboardListingRoutes", @@ -7817,7 +7853,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DashboardListingRoutes [C:4] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search]\n# @BRIEF Dashboard listing route handler for Dashboard Hub.\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\nfrom typing import Any, Literal\n\nfrom fastapi import Depends, HTTPException, Query\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_current_user,\n get_resource_service,\n get_task_manager,\n has_permission,\n)\nfrom src.models.auth import User\nfrom src.services.profile_service import ProfileService\n\nfrom ._helpers import _dashboard_git_filter_value, _normalize_filter_values\nfrom ._projection import (\n _get_profile_filter_binding,\n _matches_dashboard_actor_aliases,\n _project_dashboard_response_items,\n _resolve_profile_actor_aliases,\n)\nfrom ._router import router\nfrom ._schemas import DashboardsResponse, EffectiveProfileFilter\n\n\n# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status\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 dashboards with enhanced metadata and pagination info\n# @POST Response includes pagination metadata (page, page_size, total, total_pages)\n# @POST Response includes effective profile filter metadata for main dashboards page context\n# @RELATION CALLS -> [EXT:method:get_dashboards_with_status]\n@router.get(\"\", response_model=DashboardsResponse)\nasync def get_dashboards(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n page_context: Literal[\"dashboards_main\", \"other\"] = Query(\"dashboards_main\"),\n apply_profile_default: bool = Query(default=True),\n override_show_all: bool = Query(default=False),\n filter_title: list[str] | None = Query(default=None),\n filter_git_status: list[str] | None = Query(default=None),\n filter_llm_status: list[str] | None = Query(default=None),\n filter_changed_on: list[str] | None = Query(default=None),\n filter_actor: list[str] | None = Query(default=None),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboards\",\n f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}, \"\n f\"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}\",\n ):\n if page < 1:\n logger.error(f\"[get_dashboards][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_dashboards][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\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(f\"[get_dashboards][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n bound_username: str | None = None\n can_apply_profile_filter = False\n can_apply_slug_filter = False\n effective_profile_filter = EffectiveProfileFilter(\n applied=False,\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=None,\n match_logic=None,\n )\n profile_service: ProfileService | None = None\n\n try:\n profile_service_module = getattr(ProfileService, \"__module__\", \"\")\n is_mock_db = db.__class__.__module__.startswith(\"unittest.mock\")\n use_profile_service = (not is_mock_db) or profile_service_module.startswith(\n \"unittest.mock\"\n )\n if use_profile_service:\n profile_service = ProfileService(db=db, config_manager=config_manager)\n profile_preference = _get_profile_filter_binding(\n profile_service, current_user\n )\n normalized_username = (\n str(profile_preference.get(\"superset_username_normalized\") or \"\")\n .strip()\n .lower()\n )\n raw_username = (\n str(profile_preference.get(\"superset_username\") or \"\")\n .strip()\n .lower()\n )\n bound_username = normalized_username or raw_username or None\n\n can_apply_profile_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_my_dashboards\", False))\n and bool(bound_username)\n )\n can_apply_slug_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_slug_dashboards\", True))\n )\n\n profile_match_logic = None\n if can_apply_profile_filter and can_apply_slug_filter:\n profile_match_logic = \"owners_or_modified_by+slug_only\"\n elif can_apply_profile_filter:\n profile_match_logic = \"owners_or_modified_by\"\n elif can_apply_slug_filter:\n profile_match_logic = \"slug_only\"\n\n effective_profile_filter = EffectiveProfileFilter(\n applied=bool(can_apply_profile_filter or can_apply_slug_filter),\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=bound_username if can_apply_profile_filter else None,\n match_logic=profile_match_logic,\n )\n except Exception as profile_error:\n logger.explore(\n f\"Profile preference unavailable; continuing without profile-default filter: {profile_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n\n try:\n all_tasks = task_manager.get_all_tasks()\n title_filters = _normalize_filter_values(filter_title)\n git_filters = _normalize_filter_values(filter_git_status)\n llm_filters = _normalize_filter_values(filter_llm_status)\n changed_on_filters = _normalize_filter_values(filter_changed_on)\n actor_filters = _normalize_filter_values(filter_actor)\n has_column_filters = any(\n (\n title_filters,\n git_filters,\n llm_filters,\n changed_on_filters,\n actor_filters,\n )\n )\n needs_full_scan = (\n has_column_filters\n or bool(can_apply_profile_filter)\n or bool(can_apply_slug_filter)\n )\n\n if not needs_full_scan:\n try:\n page_payload = (\n await resource_service.get_dashboards_page_with_status(\n env,\n all_tasks,\n page=page,\n page_size=page_size,\n search=search,\n include_git_status=False,\n require_slug=bool(can_apply_slug_filter),\n )\n )\n paginated_dashboards = page_payload[\"dashboards\"]\n total = page_payload[\"total\"]\n total_pages = page_payload[\"total_pages\"]\n except Exception as page_error:\n logger.reason(\n f\"Page-based fetch failed; using compatibility fallback: {page_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n )\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n total = len(dashboards)\n total_pages = (\n (total + page_size - 1) // page_size if total > 0 else 1\n )\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n else:\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n )\n\n if (\n can_apply_profile_filter\n and bound_username\n and profile_service is not None\n ):\n actor_aliases = _resolve_profile_actor_aliases(env, bound_username)\n if not actor_aliases:\n actor_aliases = [bound_username]\n logger.reason(\n \"Applying profile actor filter\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"dashboards_before\": len(dashboards)}},\n )\n filtered_dashboards: list[dict[str, Any]] = []\n max_actor_samples = 15\n for index, dashboard in enumerate(dashboards):\n owners_value = dashboard.get(\"owners\")\n created_by_value = dashboard.get(\"created_by\")\n modified_by_value = dashboard.get(\"modified_by\")\n matches_actor = _matches_dashboard_actor_aliases(\n profile_service=profile_service,\n actor_aliases=actor_aliases,\n owners=owners_value,\n modified_by=modified_by_value,\n )\n if index < max_actor_samples:\n logger.reflect(\n \"Profile actor filter sample\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"dashboard_id\": dashboard.get('id'), \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"owners\": owners_value, \"created_by\": created_by_value, \"modified_by\": modified_by_value, \"matches\": matches_actor}},\n )\n if matches_actor:\n filtered_dashboards.append(dashboard)\n\n logger.reflect(\n \"Profile actor filter summary\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"dashboards_before\": len(dashboards), \"dashboards_after\": len(filtered_dashboards)}},\n )\n dashboards = filtered_dashboards\n\n if can_apply_slug_filter:\n dashboards = [\n dashboard\n for dashboard in dashboards\n if str(dashboard.get(\"slug\") or \"\").strip()\n ]\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n def _matches_dashboard_filters(dashboard: dict[str, Any]) -> bool:\n title_value = str(dashboard.get(\"title\") or \"\").strip().lower()\n if title_filters and title_value not in title_filters:\n return False\n\n if git_filters:\n git_value = _dashboard_git_filter_value(dashboard)\n if git_value not in git_filters:\n return False\n\n llm_value = (\n str(\n (\n (dashboard.get(\"last_task\") or {}).get(\n \"validation_status\"\n )\n )\n or \"UNKNOWN\"\n )\n .strip()\n .lower()\n )\n if llm_filters and llm_value not in llm_filters:\n return False\n\n changed_on_raw = (\n str(dashboard.get(\"last_modified\") or \"\").strip().lower()\n )\n changed_on_prefix = (\n changed_on_raw[:10]\n if len(changed_on_raw) >= 10\n else changed_on_raw\n )\n if (\n changed_on_filters\n and changed_on_raw not in changed_on_filters\n and changed_on_prefix not in changed_on_filters\n ):\n return False\n\n owners = dashboard.get(\"owners\") or []\n if isinstance(owners, list):\n actor_value = \", \".join(\n str(item).strip() for item in owners if str(item).strip()\n ).lower()\n else:\n actor_value = str(owners).strip().lower()\n if not actor_value:\n actor_value = \"-\"\n if actor_filters and actor_value not in actor_filters:\n return False\n return True\n\n if has_column_filters:\n dashboards = [\n d for d in dashboards if _matches_dashboard_filters(d)\n ]\n\n total = len(dashboards)\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 paginated_dashboards = dashboards[start_idx:end_idx]\n\n logger.info(\n f\"[get_dashboards][Coherence:OK] Returning {len(paginated_dashboards)} dashboards \"\n f\"(page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})\"\n )\n\n response_dashboards = _project_dashboard_response_items(\n paginated_dashboards\n )\n\n return DashboardsResponse(\n dashboards=response_dashboards,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages,\n effective_profile_filter=effective_profile_filter,\n )\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboards][Coherence:Failed] Failed to fetch dashboards: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboards: {e!s}\"\n )\n\n\n# #endregion get_dashboards\n# #endregion DashboardListingRoutes\n" + "body": "# #region DashboardListingRoutes [C:4] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search]\n# @BRIEF Dashboard listing route handler for Dashboard Hub.\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\nfrom typing import Any, Literal\n\nfrom fastapi import Depends, HTTPException, Query\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_current_user,\n get_resource_service,\n get_task_manager,\n has_permission,\n)\nfrom src.models.auth import User\nfrom src.services.profile_service import ProfileService\n\nfrom ._helpers import _dashboard_git_filter_value, _normalize_filter_values\nfrom ._projection import (\n _get_profile_filter_binding,\n _matches_dashboard_actor_aliases,\n _project_dashboard_response_items,\n _resolve_profile_actor_aliases,\n)\nfrom ._router import router\nfrom ._schemas import DashboardsResponse, EffectiveProfileFilter\n\n\n# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status\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 dashboards with enhanced metadata and pagination info\n# @POST Response includes pagination metadata (page, page_size, total, total_pages)\n# @POST Response includes effective profile filter metadata for main dashboards page context\n# @RELATION CALLS -> [EXT:method:get_dashboards_with_status]\n@router.get(\"\", response_model=DashboardsResponse)\nasync def get_dashboards(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n page_context: Literal[\"dashboards_main\", \"other\"] = Query(\"dashboards_main\"),\n apply_profile_default: bool = Query(default=True),\n override_show_all: bool = Query(default=False),\n filter_title: list[str] | None = Query(default=None),\n filter_git_status: list[str] | None = Query(default=None),\n filter_llm_status: list[str] | None = Query(default=None),\n filter_changed_on: list[str] | None = Query(default=None),\n filter_actor: list[str] | None = Query(default=None),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboards\",\n f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}, \"\n f\"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}\",\n ):\n if page < 1:\n logger.error(f\"[get_dashboards][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_dashboards][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\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(f\"[get_dashboards][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n bound_username: str | None = None\n can_apply_profile_filter = False\n can_apply_slug_filter = False\n effective_profile_filter = EffectiveProfileFilter(\n applied=False,\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=None,\n match_logic=None,\n )\n profile_service: ProfileService | None = None\n\n try:\n profile_service_module = getattr(ProfileService, \"__module__\", \"\")\n is_mock_db = db.__class__.__module__.startswith(\"unittest.mock\")\n use_profile_service = (not is_mock_db) or profile_service_module.startswith(\n \"unittest.mock\"\n )\n if use_profile_service:\n profile_service = ProfileService(db=db, config_manager=config_manager)\n profile_preference = _get_profile_filter_binding(\n profile_service, current_user\n )\n normalized_username = (\n str(profile_preference.get(\"superset_username_normalized\") or \"\")\n .strip()\n .lower()\n )\n raw_username = (\n str(profile_preference.get(\"superset_username\") or \"\")\n .strip()\n .lower()\n )\n bound_username = normalized_username or raw_username or None\n\n can_apply_profile_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_my_dashboards\", False))\n and bool(bound_username)\n )\n can_apply_slug_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_slug_dashboards\", True))\n )\n\n profile_match_logic = None\n if can_apply_profile_filter and can_apply_slug_filter:\n profile_match_logic = \"owners_or_modified_by+slug_only\"\n elif can_apply_profile_filter:\n profile_match_logic = \"owners_or_modified_by\"\n elif can_apply_slug_filter:\n profile_match_logic = \"slug_only\"\n\n effective_profile_filter = EffectiveProfileFilter(\n applied=bool(can_apply_profile_filter or can_apply_slug_filter),\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=bound_username if can_apply_profile_filter else None,\n match_logic=profile_match_logic,\n )\n except Exception as profile_error:\n logger.explore(\n f\"Profile preference unavailable; continuing without profile-default filter: {profile_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n\n try:\n all_tasks = task_manager.get_all_tasks()\n title_filters = _normalize_filter_values(filter_title)\n git_filters = _normalize_filter_values(filter_git_status)\n llm_filters = _normalize_filter_values(filter_llm_status)\n changed_on_filters = _normalize_filter_values(filter_changed_on)\n actor_filters = _normalize_filter_values(filter_actor)\n has_column_filters = any(\n (\n title_filters,\n git_filters,\n llm_filters,\n changed_on_filters,\n actor_filters,\n )\n )\n needs_full_scan = (\n has_column_filters\n or bool(can_apply_profile_filter)\n or bool(can_apply_slug_filter)\n )\n\n if not needs_full_scan:\n try:\n page_payload = (\n await resource_service.get_dashboards_page_with_status(\n env,\n all_tasks,\n page=page,\n page_size=page_size,\n search=search,\n include_git_status=False,\n require_slug=bool(can_apply_slug_filter),\n )\n )\n paginated_dashboards = page_payload[\"dashboards\"]\n total = page_payload[\"total\"]\n total_pages = page_payload[\"total_pages\"]\n except Exception as page_error:\n logger.reason(\n f\"Page-based fetch failed; using compatibility fallback: {page_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n )\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n total = len(dashboards)\n total_pages = (\n (total + page_size - 1) // page_size if total > 0 else 1\n )\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n else:\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n )\n\n if (\n can_apply_profile_filter\n and bound_username\n and profile_service is not None\n ):\n actor_aliases = _resolve_profile_actor_aliases(env, bound_username)\n if not actor_aliases:\n actor_aliases = [bound_username]\n logger.reason(\n \"Applying profile actor filter\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"dashboards_before\": len(dashboards)}},\n )\n filtered_dashboards: list[dict[str, Any]] = []\n max_actor_samples = 15\n for index, dashboard in enumerate(dashboards):\n owners_value = dashboard.get(\"owners\")\n created_by_value = dashboard.get(\"created_by\")\n modified_by_value = dashboard.get(\"modified_by\")\n matches_actor = _matches_dashboard_actor_aliases(\n profile_service=profile_service,\n actor_aliases=actor_aliases,\n owners=owners_value,\n modified_by=modified_by_value,\n )\n if index < max_actor_samples:\n logger.reflect(\n \"Profile actor filter sample\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"dashboard_id\": dashboard.get('id'), \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"owners\": owners_value, \"created_by\": created_by_value, \"modified_by\": modified_by_value, \"matches\": matches_actor}},\n )\n if matches_actor:\n filtered_dashboards.append(dashboard)\n\n logger.reflect(\n \"Profile actor filter summary\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"dashboards_before\": len(dashboards), \"dashboards_after\": len(filtered_dashboards)}},\n )\n dashboards = filtered_dashboards\n\n if can_apply_slug_filter:\n dashboards = [\n dashboard\n for dashboard in dashboards\n if str(dashboard.get(\"slug\") or \"\").strip()\n ]\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n def _matches_dashboard_filters(dashboard: dict[str, Any]) -> bool:\n title_value = str(dashboard.get(\"title\") or \"\").strip().lower()\n if title_filters and title_value not in title_filters:\n return False\n\n if git_filters:\n git_value = _dashboard_git_filter_value(dashboard)\n if git_value not in git_filters:\n return False\n\n llm_value = (\n str(\n (\n (dashboard.get(\"last_task\") or {}).get(\n \"validation_status\"\n )\n )\n or \"UNKNOWN\"\n )\n .strip()\n .lower()\n )\n if llm_filters and llm_value not in llm_filters:\n return False\n\n changed_on_raw = (\n str(dashboard.get(\"last_modified\") or \"\").strip().lower()\n )\n changed_on_prefix = (\n changed_on_raw[:10]\n if len(changed_on_raw) >= 10\n else changed_on_raw\n )\n if (\n changed_on_filters\n and changed_on_raw not in changed_on_filters\n and changed_on_prefix not in changed_on_filters\n ):\n return False\n\n owners = dashboard.get(\"owners\") or []\n if isinstance(owners, list):\n actor_value = \", \".join(\n str(item).strip() for item in owners if str(item).strip()\n ).lower()\n else:\n actor_value = str(owners).strip().lower()\n if not actor_value:\n actor_value = \"-\"\n if actor_filters and actor_value not in actor_filters:\n return False\n return True\n\n if has_column_filters:\n dashboards = [\n d for d in dashboards if _matches_dashboard_filters(d)\n ]\n\n total = len(dashboards)\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 paginated_dashboards = dashboards[start_idx:end_idx]\n\n logger.info(\n f\"[get_dashboards][Coherence:OK] Returning {len(paginated_dashboards)} dashboards \"\n f\"(page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})\"\n )\n\n response_dashboards = _project_dashboard_response_items(\n paginated_dashboards\n )\n\n return DashboardsResponse(\n dashboards=response_dashboards,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages,\n effective_profile_filter=effective_profile_filter,\n )\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboards][Coherence:Failed] Failed to fetch dashboards: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboards: {e!s}\"\n )\n\n\n# #endregion get_dashboards\n# #endregion DashboardListingRoutes\n" }, { "contract_id": "get_dashboards", @@ -10375,14 +10411,14 @@ { "source_id": "GitPackage", "relation_type": "CALLS", - "target_id": "[EXT:list:GitPackage_all_routes]", - "target_ref": "[[EXT:list:GitPackage_all_routes]]" + "target_id": "GitPackage", + "target_ref": "[GitPackage]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitPackage [C:5] [TYPE Module] [SEMANTICS git, api, package, sync]\n# @BRIEF Package root for decomposed git routes. Re-exports all public symbols from submodules.\n# @LAYER API\n# @RELATION CALLS -> [[EXT:list:GitPackage_all_routes]]\n# @INVARIANT git_service and os are module-level attributes for test monkeypatch compatibility.\n# @PRE Git service initialized\n# @POST Git API package exported\n# @SIDE_EFFECT Registers git route submodules\n# @DATA_CONTRACT GitRequest -> GitResponse\n# All route functions are re-exported for direct access via `from src.api.routes import git`.\n\nimport os\n\nfrom src.services.git_service import GitService\n\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH\nfrom ._router import router\n\n# -- git_service singleton (canonical instance; tests may monkeypatch git_routes.git_service) --\ngit_service = GitService()\n\n# -- Config routes --\nfrom ._config_routes import create_git_config, delete_git_config, get_git_configs, test_git_config, update_git_config # noqa: F401\n\n# -- Environment routes --\nfrom ._environment_routes import get_environments # noqa: F401\n\n# -- Gitea routes --\nfrom ._gitea_routes import create_gitea_repository, create_remote_repository, delete_gitea_repository, list_gitea_repositories # noqa: F401\nfrom ._helpers import _resolve_dashboard_id_from_ref, _resolve_dashboard_id_from_ref_async, _resolve_repo_key_from_ref # noqa: F401 — re-exported for test monkeypatch compatibility\n\n# -- Merge routes --\nfrom ._merge_routes import abort_merge, continue_merge, get_merge_conflicts, get_merge_status, resolve_merge_conflicts # noqa: F401\n\n# -- Repo lifecycle routes (sync, promote, deploy) --\nfrom ._repo_lifecycle_routes import deploy_dashboard, promote_dashboard, sync_dashboard # noqa: F401\n\n# -- Repo operations routes (commit, push, pull, status, diff, history, generate-message) --\nfrom ._repo_operations_routes import commit_changes, generate_commit_message, get_history, get_repository_diff, get_repository_status, get_repository_status_batch, pull_changes, push_changes # noqa: F401\n\n# -- Repo routes (core) --\nfrom ._repo_routes import checkout_branch, create_branch, delete_repository, get_branches, get_repository_binding, init_repository # noqa: F401\n\n# #endregion GitPackage\n" + "body": "# #region GitPackage [C:5] [TYPE Module] [SEMANTICS git, api, package, sync]\n# @BRIEF Package root for decomposed git routes. Re-exports all public symbols from submodules.\n# @LAYER API\n# @RELATION CALLS -> [GitPackage]\n# @INVARIANT git_service and os are module-level attributes for test monkeypatch compatibility.\n# @PRE Git service initialized\n# @POST Git API package exported\n# @SIDE_EFFECT Registers git route submodules\n# @DATA_CONTRACT GitRequest -> GitResponse\n# All route functions are re-exported for direct access via `from src.api.routes import git`.\n\nimport os\n\nfrom src.services.git_service import GitService\n\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH\nfrom ._router import router\n\n# -- git_service singleton (canonical instance; tests may monkeypatch git_routes.git_service) --\ngit_service = GitService()\n\n# -- Config routes --\nfrom ._config_routes import create_git_config, delete_git_config, get_git_configs, test_git_config, update_git_config # noqa: F401\n\n# -- Environment routes --\nfrom ._environment_routes import get_environments # noqa: F401\n\n# -- Gitea routes --\nfrom ._gitea_routes import create_gitea_repository, create_remote_repository, delete_gitea_repository, list_gitea_repositories # noqa: F401\nfrom ._helpers import _resolve_dashboard_id_from_ref, _resolve_dashboard_id_from_ref_async, _resolve_repo_key_from_ref # noqa: F401 — re-exported for test monkeypatch compatibility\n\n# -- Merge routes --\nfrom ._merge_routes import abort_merge, continue_merge, get_merge_conflicts, get_merge_status, resolve_merge_conflicts # noqa: F401\n\n# -- Repo lifecycle routes (sync, promote, deploy) --\nfrom ._repo_lifecycle_routes import deploy_dashboard, promote_dashboard, sync_dashboard # noqa: F401\n\n# -- Repo operations routes (commit, push, pull, status, diff, history, generate-message) --\nfrom ._repo_operations_routes import commit_changes, generate_commit_message, get_history, get_repository_diff, get_repository_status, get_repository_status_batch, pull_changes, push_changes # noqa: F401\n\n# -- Repo routes (core) --\nfrom ._repo_routes import checkout_branch, create_branch, delete_repository, get_branches, get_repository_binding, init_repository # noqa: F401\n\n# #endregion GitPackage\n" }, { "contract_id": "GitConfigRoutes", @@ -11357,14 +11393,14 @@ { "source_id": "GitSchemas", "relation_type": "DEPENDS_ON", - "target_id": "EXT:path:backend.src.models.git", - "target_ref": "[EXT:path:backend.src.models.git]" + "target_id": "GitModels", + "target_ref": "[GitModels]" } ], "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 -> [EXT:path:backend.src.models.git]\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, 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" }, { "contract_id": "GitServerConfigBase", @@ -11850,7 +11886,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region health_router [C:3] [TYPE Module] [SEMANTICS fastapi, health, api, search, dashboard]\n# @BRIEF API endpoints for dashboard health monitoring and status aggregation.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [health_service]\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...schemas.health import HealthSummaryResponse\nfrom ...services.health_service import HealthService\n\nrouter = APIRouter(prefix=\"/api/health\", tags=[\"Health\"])\n\n# #region get_health_summary [TYPE Function]\n# @BRIEF Get aggregated health status for all dashboards.\n# @PRE Caller has read permission for dashboard health view.\n# @POST Returns HealthSummaryResponse.\n# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: str | None = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# #endregion get_health_summary\n\n\n# #region delete_health_report [TYPE Function]\n# @BRIEF Delete one persisted dashboard validation report from health summary.\n# @PRE Caller has write permission for tasks/report maintenance.\n# @POST Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# #endregion delete_health_report\n\n# #endregion health_router\n" + "body": "# #region health_router [C:3] [TYPE Module] [SEMANTICS fastapi, health, api, search, dashboard]\n# @BRIEF API endpoints for dashboard health monitoring and status aggregation.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [health_service]\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...schemas.health import HealthSummaryResponse\nfrom ...services.health_service import HealthService\n\nrouter = APIRouter(prefix=\"/api/health\", tags=[\"Health\"])\n\n# #region get_health_summary [TYPE Function]\n# @BRIEF Get aggregated health status for all dashboards.\n# @PRE Caller has read permission for dashboard health view.\n# @POST Returns HealthSummaryResponse.\n# @RELATION CALLS -> [HealthService]\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: str | None = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# #endregion get_health_summary\n\n\n# #region delete_health_report [TYPE Function]\n# @BRIEF Delete one persisted dashboard validation report from health summary.\n# @PRE Caller has write permission for tasks/report maintenance.\n# @POST Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION CALLS -> [HealthService]\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# #endregion delete_health_report\n\n# #endregion health_router\n" }, { "contract_id": "get_health_summary", @@ -11869,14 +11905,14 @@ { "source_id": "get_health_summary", "relation_type": "CALLS", - "target_id": "EXT:path:backend.src.services.health_service.HealthService", - "target_ref": "[EXT:path:backend.src.services.health_service.HealthService]" + "target_id": "HealthService", + "target_ref": "[HealthService]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_health_summary [TYPE Function]\n# @BRIEF Get aggregated health status for all dashboards.\n# @PRE Caller has read permission for dashboard health view.\n# @POST Returns HealthSummaryResponse.\n# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: str | None = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# #endregion get_health_summary\n" + "body": "# #region get_health_summary [TYPE Function]\n# @BRIEF Get aggregated health status for all dashboards.\n# @PRE Caller has read permission for dashboard health view.\n# @POST Returns HealthSummaryResponse.\n# @RELATION CALLS -> [HealthService]\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: str | None = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# #endregion get_health_summary\n" }, { "contract_id": "delete_health_report", @@ -11895,14 +11931,14 @@ { "source_id": "delete_health_report", "relation_type": "CALLS", - "target_id": "EXT:path:backend.src.services.health_service.HealthService", - "target_ref": "[EXT:path:backend.src.services.health_service.HealthService]" + "target_id": "HealthService", + "target_ref": "[HealthService]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region delete_health_report [TYPE Function]\n# @BRIEF Delete one persisted dashboard validation report from health summary.\n# @PRE Caller has write permission for tasks/report maintenance.\n# @POST Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# #endregion delete_health_report\n" + "body": "# #region delete_health_report [TYPE Function]\n# @BRIEF Delete one persisted dashboard validation report from health summary.\n# @PRE Caller has write permission for tasks/report maintenance.\n# @POST Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION CALLS -> [HealthService]\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# #endregion delete_health_report\n" }, { "contract_id": "LlmRoutes", @@ -12382,7 +12418,7 @@ "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 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" }, { "contract_id": "list_dashboard_banners", @@ -12400,14 +12436,14 @@ { "source_id": "list_dashboard_banners", "relation_type": "DEPENDS_ON", - "target_id": "EXT:code:has_permission(\"maintenance\", \"READ\")", - "target_ref": "[EXT:code:has_permission(\"maintenance\", \"READ\")]" + "target_id": "EXT:code:has_permission_maintenance_READ", + "target_ref": "[EXT:code:has_permission_maintenance_READ]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #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" + "body": "# #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" }, { "contract_id": "list_events", @@ -12425,14 +12461,14 @@ { "source_id": "list_events", "relation_type": "DEPENDS_ON", - "target_id": "EXT:code:has_permission(\"maintenance\", \"READ\")", - "target_ref": "[EXT:code:has_permission(\"maintenance\", \"READ\")]" + "target_id": "EXT:code:has_permission_maintenance_READ", + "target_ref": "[EXT:code:has_permission_maintenance_READ]" } ], "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(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" }, { "contract_id": "start_maintenance", @@ -12504,14 +12540,14 @@ { "source_id": "get_maintenance_settings", "relation_type": "DEPENDS_ON", - "target_id": "EXT:code:has_permission(\"maintenance\", \"READ\")", - "target_ref": "[EXT:code:has_permission(\"maintenance\", \"READ\")]" + "target_id": "EXT:code:has_permission_maintenance_READ", + "target_ref": "[EXT:code:has_permission_maintenance_READ]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #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" + "body": "# #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" }, { "contract_id": "update_maintenance_settings", @@ -16943,14 +16979,14 @@ { "source_id": "NativeFilterExtractionTests", "relation_type": "BINDS_TO", - "target_id": "[EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge]", - "target_ref": "[[EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge]]" + "target_id": "FilterState", + "target_ref": "[FilterState]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region NativeFilterExtractionTests [TYPE Module] [C:3] [SEMANTICS tests, superset, native, filters, permalink, filter_state]\n# @BRIEF Verify native filter extraction from permalinks and native_filters_key URLs.\n# @LAYER Domain\n# @RELATION BINDS_TO ->[SupersetClient]\n# @RELATION BINDS_TO ->[AsyncSupersetClient]\n# @RELATION BINDS_TO ->[[EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge]]\nimport json\nfrom unittest.mock import MagicMock\n\nfrom src.core.config_models import Environment\nfrom src.core.superset_client import SupersetClient\nfrom src.core.utils.superset_context_extractor import (\n SupersetContextExtractor,\n SupersetParsedContext,\n sanitize_imported_filter_for_assistant,\n)\nfrom src.models.filter_state import (\n ExtraFormDataMerge,\n FilterState,\n NativeFilterDataMask,\n ParsedNativeFilters,\n)\n\n\n# #region _make_environment [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\ndef _make_environment() -> Environment:\n return Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n# #endregion _make_environment\n# #region test_extract_native_filters_from_permalink [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Extract native filters from a permalink key.\ndef test_extract_native_filters_from_permalink():\n client = SupersetClient(_make_environment())\n client.get_dashboard_permalink_state = MagicMock(\n return_value={\n \"result\": {\n \"state\": {\n \"dataMask\": {\n \"filter_country\": {\n \"extraFormData\": {\n \"filters\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n },\n \"filterState\": {\"value\": [\"DE\", \"FR\"]},\n \"ownState\": {},\n },\n \"filter_date\": {\n \"extraFormData\": {\"time_range\": \"2020-01-01 : 2024-12-31\"},\n \"filterState\": {\"value\": \"2020-01-01 : 2024-12-31\"},\n \"ownState\": {},\n },\n },\n \"activeTabs\": [\"tab1\", \"tab2\"],\n \"anchor\": \"SECTION1\",\n \"chartStates\": {\"chart_1\": {}},\n }\n }\n }\n )\n result = client.extract_native_filters_from_permalink(\"test-permalink-key\")\n assert result[\"permalink_key\"] == \"test-permalink-key\"\n assert \"dataMask\" in result\n assert \"filter_country\" in result[\"dataMask\"]\n assert \"filter_date\" in result[\"dataMask\"]\n assert result[\"dataMask\"][\"filter_country\"][\"extraFormData\"][\"filters\"][0][\n \"val\"\n ] == [\"DE\", \"FR\"]\n assert result[\"activeTabs\"] == [\"tab1\", \"tab2\"]\n assert result[\"anchor\"] == \"SECTION1\"\n# #endregion test_extract_native_filters_from_permalink\n# #region test_extract_native_filters_from_permalink_direct_response [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle permalink response without result wrapper.\ndef test_extract_native_filters_from_permalink_direct_response():\n client = SupersetClient(_make_environment())\n client.get_dashboard_permalink_state = MagicMock(\n return_value={\n \"state\": {\n \"dataMask\": {\n \"filter_1\": {\n \"extraFormData\": {\"filters\": []},\n \"filterState\": {},\n \"ownState\": {},\n }\n }\n }\n }\n )\n result = client.extract_native_filters_from_permalink(\"direct-key\")\n assert result[\"permalink_key\"] == \"direct-key\"\n assert \"filter_1\" in result[\"dataMask\"]\n# #endregion test_extract_native_filters_from_permalink_direct_response\n# #region test_extract_native_filters_from_key [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Extract native filters from a native_filters_key.\ndef test_extract_native_filters_from_key():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": json.dumps(\n {\n \"filter_region\": {\n \"id\": \"filter_region\",\n \"extraFormData\": {\n \"filters\": [\n {\"col\": \"region\", \"op\": \"IN\", \"val\": [\"EMEA\"]}\n ]\n },\n \"filterState\": {\"value\": [\"EMEA\"]},\n }\n }\n )\n }\n }\n )\n result = client.extract_native_filters_from_key(123, \"filter-state-key\")\n assert result[\"dashboard_id\"] == 123\n assert result[\"filter_state_key\"] == \"filter-state-key\"\n assert \"dataMask\" in result\n assert \"filter_region\" in result[\"dataMask\"]\n assert result[\"dataMask\"][\"filter_region\"][\"extraFormData\"][\"filters\"][0][\n \"val\"\n ] == [\"EMEA\"]\n# #endregion test_extract_native_filters_from_key\n# #region test_extract_native_filters_from_key_single_filter [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle single filter format in native filter state.\ndef test_extract_native_filters_from_key_single_filter():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": json.dumps(\n {\n \"id\": \"single_filter\",\n \"extraFormData\": {\n \"filters\": [{\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"}]\n },\n \"filterState\": {\"value\": \"active\"},\n }\n )\n }\n }\n )\n result = client.extract_native_filters_from_key(456, \"single-key\")\n assert \"dataMask\" in result\n assert \"single_filter\" in result[\"dataMask\"]\n assert (\n result[\"dataMask\"][\"single_filter\"][\"extraFormData\"][\"filters\"][0][\"col\"]\n == \"status\"\n )\n# #endregion test_extract_native_filters_from_key_single_filter\n# #region test_extract_native_filters_from_key_dict_value [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle filter state value as dict instead of JSON string.\ndef test_extract_native_filters_from_key_dict_value():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": {\n \"filter_id\": {\n \"extraFormData\": {\"filters\": []},\n \"filterState\": {},\n }\n }\n }\n }\n )\n result = client.extract_native_filters_from_key(789, \"dict-key\")\n assert \"dataMask\" in result\n assert \"filter_id\" in result[\"dataMask\"]\n# #endregion test_extract_native_filters_from_key_dict_value\n# #region test_parse_dashboard_url_for_filters_permalink [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse permalink URL format.\ndef test_parse_dashboard_url_for_filters_permalink():\n client = SupersetClient(_make_environment())\n client.extract_native_filters_from_permalink = MagicMock(\n return_value={\"dataMask\": {\"f1\": {}}, \"permalink_key\": \"abc123\"}\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/superset/dashboard/p/abc123/\"\n )\n assert result[\"filter_type\"] == \"permalink\"\n assert result[\"filters\"][\"dataMask\"][\"f1\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_permalink\n# #region test_parse_dashboard_url_for_filters_native_key [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters_key URL format with numeric dashboard ID.\ndef test_parse_dashboard_url_for_filters_native_key():\n client = SupersetClient(_make_environment())\n client.extract_native_filters_from_key = MagicMock(\n return_value={\n \"dataMask\": {\"f2\": {}},\n \"dashboard_id\": 42,\n \"filter_state_key\": \"xyz\",\n }\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/42/?native_filters_key=xyz\"\n )\n assert result[\"filter_type\"] == \"native_filters_key\"\n assert result[\"dashboard_id\"] == 42\n assert result[\"filters\"][\"dataMask\"][\"f2\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_native_key\n# #region test_parse_dashboard_url_for_filters_native_key_slug [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters_key URL format when dashboard reference is a slug, not a numeric ID.\ndef test_parse_dashboard_url_for_filters_native_key_slug():\n client = SupersetClient(_make_environment())\n # Simulate slug resolution: get_dashboard returns the dashboard with numeric ID\n client.get_dashboard = MagicMock(\n return_value={\n \"result\": {\"id\": 99, \"dashboard_title\": \"COVID Dashboard\", \"slug\": \"covid\"}\n }\n )\n client.extract_native_filters_from_key = MagicMock(\n return_value={\n \"dataMask\": {\"f_slug\": {}},\n \"dashboard_id\": 99,\n \"filter_state_key\": \"abc123\",\n }\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/superset/dashboard/covid/?native_filters_key=abc123\"\n )\n assert result[\"filter_type\"] == \"native_filters_key\"\n assert result[\"dashboard_id\"] == 99\n assert result[\"filters\"][\"dataMask\"][\"f_slug\"] == {}\n client.get_dashboard.assert_called_once_with(\"covid\")\n client.extract_native_filters_from_key.assert_called_once_with(99, \"abc123\")\n# #endregion test_parse_dashboard_url_for_filters_native_key_slug\n# #region test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Gracefully handle slug resolution failure for native_filters_key URL.\ndef test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails():\n client = SupersetClient(_make_environment())\n client.get_dashboard = MagicMock(side_effect=Exception(\"Not found\"))\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/unknownslug/?native_filters_key=key1\"\n )\n assert result[\"filter_type\"] is None\n assert result[\"dashboard_id\"] is None\n# #endregion test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails\n# #region test_parse_dashboard_url_for_filters_native_filters_direct [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters direct query param.\ndef test_parse_dashboard_url_for_filters_native_filters_direct():\n client = SupersetClient(_make_environment())\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/1/?native_filters=\"\n + json.dumps({\"filter_1\": {\"col\": \"x\", \"op\": \"==\", \"val\": \"y\"}})\n )\n assert result[\"filter_type\"] == \"native_filters\"\n assert \"dataMask\" in result[\"filters\"]\n# #endregion test_parse_dashboard_url_for_filters_native_filters_direct\n# #region test_parse_dashboard_url_for_filters_no_filters [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Return empty result when no filters present.\ndef test_parse_dashboard_url_for_filters_no_filters():\n client = SupersetClient(_make_environment())\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/1/\"\n )\n assert result[\"filter_type\"] is None\n assert result[\"filters\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_no_filters\n# #region test_extra_form_data_merge [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ExtraFormDataMerge correctly merges dictionaries.\ndef test_extra_form_data_merge():\n merger = ExtraFormDataMerge()\n original = {\n \"filters\": [{\"col\": \"a\", \"op\": \"IN\", \"val\": [1, 2]}],\n \"time_range\": \"2020-01-01 : 2021-01-01\",\n \"extras\": {\"where\": \"x > 0\"},\n }\n new = {\n \"filters\": [{\"col\": \"b\", \"op\": \"==\", \"val\": \"test\"}],\n \"time_range\": \"2022-01-01 : 2023-01-01\",\n \"columns\": [\"col1\", \"col2\"],\n }\n result = merger.merge(original, new)\n # Filters should be appended\n assert len(result[\"filters\"]) == 2\n assert result[\"filters\"][0][\"col\"] == \"a\"\n assert result[\"filters\"][1][\"col\"] == \"b\"\n # Time range should be overridden\n assert result[\"time_range\"] == \"2022-01-01 : 2023-01-01\"\n # Extras should remain\n assert result[\"extras\"] == {\"where\": \"x > 0\"}\n # New columns should be added\n assert result[\"columns\"] == [\"col1\", \"col2\"]\n# #endregion test_extra_form_data_merge\n# #region test_filter_state_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test FilterState Pydantic model.\ndef test_filter_state_model():\n state = FilterState(\n extraFormData={\"filters\": [{\"col\": \"x\", \"op\": \"==\", \"val\": \"y\"}]},\n filterState={\"value\": \"y\"},\n ownState={\"selectedValues\": [\"y\"]},\n )\n assert state.extraFormData[\"filters\"][0][\"col\"] == \"x\"\n assert state.filterState[\"value\"] == \"y\"\n assert state.ownState[\"selectedValues\"] == [\"y\"]\n# #endregion test_filter_state_model\n# #region test_parsed_native_filters_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ParsedNativeFilters Pydantic model.\ndef test_parsed_native_filters_model():\n filters = ParsedNativeFilters(\n dataMask={\"f1\": {\"extraFormData\": {}, \"filterState\": {}}},\n filter_type=\"permalink\",\n dashboard_id=\"42\",\n permalink_key=\"abc\",\n )\n assert filters.has_filters() is True\n assert filters.get_filter_count() == 1\n assert filters.filter_type == \"permalink\"\n# #endregion test_parsed_native_filters_model\n# #region test_parsed_native_filters_empty [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ParsedNativeFilters with no filters.\ndef test_parsed_native_filters_empty():\n filters = ParsedNativeFilters()\n assert filters.has_filters() is False\n assert filters.get_filter_count() == 0\n# #endregion test_parsed_native_filters_empty\n# #region test_native_filter_data_mask_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test NativeFilterDataMask model.\ndef test_native_filter_data_mask_model():\n data_mask = NativeFilterDataMask(\n filters={\n \"filter_1\": FilterState(extraFormData={\"filters\": []}, filterState={}),\n \"filter_2\": FilterState(\n extraFormData={\"time_range\": \"...\"}, filterState={}\n ),\n }\n )\n assert data_mask.get_filter_ids() == [\"filter_1\", \"filter_2\"]\n assert data_mask.get_extra_form_data(\"filter_1\") == {\"filters\": []}\n assert data_mask.get_extra_form_data(\"nonexistent\") == {}\n# #endregion test_native_filter_data_mask_model\n# #region test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Reconcile raw native filter ids from state to canonical metadata filter names.\ndef test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"normalized_value\": {\n \"filter_clauses\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 1\n assert result[0][\"filter_name\"] == \"Country\"\n assert result[0][\"display_name\"] == \"Country\"\n assert result[0][\"raw_value\"] == [\"DE\", \"FR\"]\n assert result[0][\"source\"] == \"superset_native_filters_key\"\n assert result[0][\"normalized_value\"] == {\n \"filter_clauses\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n }\n# #endregion test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names\n# #region test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Collapse raw-id state entries and metadata entries into one canonical filter.\ndef test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n },\n {\n \"id\": \"NATIVE_FILTER-vv123\",\n \"name\": \"Region\",\n \"label\": \"Region\",\n },\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 2\n country_filter = next(item for item in result if item[\"filter_name\"] == \"Country\")\n region_filter = next(item for item in result if item[\"filter_name\"] == \"Region\")\n assert country_filter[\"raw_value\"] == [\"DE\", \"FR\"]\n assert country_filter[\"recovery_status\"] == \"recovered\"\n assert region_filter[\"raw_value\"] is None\n assert region_filter[\"recovery_status\"] == \"partial\"\n# #endregion test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter\n# #region test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Preserve unmatched raw native filter ids as fallback diagnostics when metadata mapping is unavailable.\ndef test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"display_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"raw_value\": [\"orphan\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 2\n assert any(\n item[\"filter_name\"] == \"Country\" and item[\"recovery_status\"] == \"partial\"\n for item in result\n )\n assert any(\n item[\"filter_name\"] == \"UNKNOWN_NATIVE_FILTER\"\n and item[\"raw_value\"] == [\"orphan\"]\n and item[\"source\"] == \"superset_native_filters_key\"\n for item in result\n )\n# #endregion test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids\n# #region test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Recovered native filter state should preserve exact Superset clause payload and time extras for preview compilation.\ndef test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview():\n extractor = SupersetContextExtractor(_make_environment(), client=MagicMock())\n imported_filters = extractor._extract_imported_filters(\n {\n \"native_filter_state\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"NATIVE_FILTER-1\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n }\n }\n }\n )\n assert imported_filters == [\n {\n \"filter_name\": \"NATIVE_FILTER-1\",\n \"raw_value\": [\"DE\"],\n \"display_name\": \"Country\",\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ]\n# #endregion test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview\n# #region test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Assistant-facing imported-filter payload should redact likely PII while preserving structure metadata.\ndef test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values():\n result = sanitize_imported_filter_for_assistant(\n {\n \"filter_name\": \"customer_email\",\n \"raw_value\": [\"alice@example.com\", \"1234567890\"],\n \"normalized_value\": {\"filter_clauses\": []},\n \"source\": \"manual\",\n }\n )\n assert result[\"raw_value\"] == [\"***@example.com\", \"********90\"]\n assert result[\"raw_value_masked\"] is True\n assert result[\"normalized_value\"] == {\"filter_clauses\": []}\n# #endregion test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values\n# #endregion NativeFilterExtractionTests\n" + "body": "# #region NativeFilterExtractionTests [TYPE Module] [C:3] [SEMANTICS tests, superset, native, filters, permalink, filter_state]\n# @BRIEF Verify native filter extraction from permalinks and native_filters_key URLs.\n# @LAYER Domain\n# @RELATION BINDS_TO ->[SupersetClient]\n# @RELATION BINDS_TO ->[AsyncSupersetClient]\n# @RELATION BINDS_TO ->[FilterState]\nimport json\nfrom unittest.mock import MagicMock\n\nfrom src.core.config_models import Environment\nfrom src.core.superset_client import SupersetClient\nfrom src.core.utils.superset_context_extractor import (\n SupersetContextExtractor,\n SupersetParsedContext,\n sanitize_imported_filter_for_assistant,\n)\nfrom src.models.filter_state import (\n ExtraFormDataMerge,\n FilterState,\n NativeFilterDataMask,\n ParsedNativeFilters,\n)\n\n\n# #region _make_environment [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\ndef _make_environment() -> Environment:\n return Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n# #endregion _make_environment\n# #region test_extract_native_filters_from_permalink [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Extract native filters from a permalink key.\ndef test_extract_native_filters_from_permalink():\n client = SupersetClient(_make_environment())\n client.get_dashboard_permalink_state = MagicMock(\n return_value={\n \"result\": {\n \"state\": {\n \"dataMask\": {\n \"filter_country\": {\n \"extraFormData\": {\n \"filters\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n },\n \"filterState\": {\"value\": [\"DE\", \"FR\"]},\n \"ownState\": {},\n },\n \"filter_date\": {\n \"extraFormData\": {\"time_range\": \"2020-01-01 : 2024-12-31\"},\n \"filterState\": {\"value\": \"2020-01-01 : 2024-12-31\"},\n \"ownState\": {},\n },\n },\n \"activeTabs\": [\"tab1\", \"tab2\"],\n \"anchor\": \"SECTION1\",\n \"chartStates\": {\"chart_1\": {}},\n }\n }\n }\n )\n result = client.extract_native_filters_from_permalink(\"test-permalink-key\")\n assert result[\"permalink_key\"] == \"test-permalink-key\"\n assert \"dataMask\" in result\n assert \"filter_country\" in result[\"dataMask\"]\n assert \"filter_date\" in result[\"dataMask\"]\n assert result[\"dataMask\"][\"filter_country\"][\"extraFormData\"][\"filters\"][0][\n \"val\"\n ] == [\"DE\", \"FR\"]\n assert result[\"activeTabs\"] == [\"tab1\", \"tab2\"]\n assert result[\"anchor\"] == \"SECTION1\"\n# #endregion test_extract_native_filters_from_permalink\n# #region test_extract_native_filters_from_permalink_direct_response [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle permalink response without result wrapper.\ndef test_extract_native_filters_from_permalink_direct_response():\n client = SupersetClient(_make_environment())\n client.get_dashboard_permalink_state = MagicMock(\n return_value={\n \"state\": {\n \"dataMask\": {\n \"filter_1\": {\n \"extraFormData\": {\"filters\": []},\n \"filterState\": {},\n \"ownState\": {},\n }\n }\n }\n }\n )\n result = client.extract_native_filters_from_permalink(\"direct-key\")\n assert result[\"permalink_key\"] == \"direct-key\"\n assert \"filter_1\" in result[\"dataMask\"]\n# #endregion test_extract_native_filters_from_permalink_direct_response\n# #region test_extract_native_filters_from_key [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Extract native filters from a native_filters_key.\ndef test_extract_native_filters_from_key():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": json.dumps(\n {\n \"filter_region\": {\n \"id\": \"filter_region\",\n \"extraFormData\": {\n \"filters\": [\n {\"col\": \"region\", \"op\": \"IN\", \"val\": [\"EMEA\"]}\n ]\n },\n \"filterState\": {\"value\": [\"EMEA\"]},\n }\n }\n )\n }\n }\n )\n result = client.extract_native_filters_from_key(123, \"filter-state-key\")\n assert result[\"dashboard_id\"] == 123\n assert result[\"filter_state_key\"] == \"filter-state-key\"\n assert \"dataMask\" in result\n assert \"filter_region\" in result[\"dataMask\"]\n assert result[\"dataMask\"][\"filter_region\"][\"extraFormData\"][\"filters\"][0][\n \"val\"\n ] == [\"EMEA\"]\n# #endregion test_extract_native_filters_from_key\n# #region test_extract_native_filters_from_key_single_filter [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle single filter format in native filter state.\ndef test_extract_native_filters_from_key_single_filter():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": json.dumps(\n {\n \"id\": \"single_filter\",\n \"extraFormData\": {\n \"filters\": [{\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"}]\n },\n \"filterState\": {\"value\": \"active\"},\n }\n )\n }\n }\n )\n result = client.extract_native_filters_from_key(456, \"single-key\")\n assert \"dataMask\" in result\n assert \"single_filter\" in result[\"dataMask\"]\n assert (\n result[\"dataMask\"][\"single_filter\"][\"extraFormData\"][\"filters\"][0][\"col\"]\n == \"status\"\n )\n# #endregion test_extract_native_filters_from_key_single_filter\n# #region test_extract_native_filters_from_key_dict_value [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle filter state value as dict instead of JSON string.\ndef test_extract_native_filters_from_key_dict_value():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": {\n \"filter_id\": {\n \"extraFormData\": {\"filters\": []},\n \"filterState\": {},\n }\n }\n }\n }\n )\n result = client.extract_native_filters_from_key(789, \"dict-key\")\n assert \"dataMask\" in result\n assert \"filter_id\" in result[\"dataMask\"]\n# #endregion test_extract_native_filters_from_key_dict_value\n# #region test_parse_dashboard_url_for_filters_permalink [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse permalink URL format.\ndef test_parse_dashboard_url_for_filters_permalink():\n client = SupersetClient(_make_environment())\n client.extract_native_filters_from_permalink = MagicMock(\n return_value={\"dataMask\": {\"f1\": {}}, \"permalink_key\": \"abc123\"}\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/superset/dashboard/p/abc123/\"\n )\n assert result[\"filter_type\"] == \"permalink\"\n assert result[\"filters\"][\"dataMask\"][\"f1\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_permalink\n# #region test_parse_dashboard_url_for_filters_native_key [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters_key URL format with numeric dashboard ID.\ndef test_parse_dashboard_url_for_filters_native_key():\n client = SupersetClient(_make_environment())\n client.extract_native_filters_from_key = MagicMock(\n return_value={\n \"dataMask\": {\"f2\": {}},\n \"dashboard_id\": 42,\n \"filter_state_key\": \"xyz\",\n }\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/42/?native_filters_key=xyz\"\n )\n assert result[\"filter_type\"] == \"native_filters_key\"\n assert result[\"dashboard_id\"] == 42\n assert result[\"filters\"][\"dataMask\"][\"f2\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_native_key\n# #region test_parse_dashboard_url_for_filters_native_key_slug [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters_key URL format when dashboard reference is a slug, not a numeric ID.\ndef test_parse_dashboard_url_for_filters_native_key_slug():\n client = SupersetClient(_make_environment())\n # Simulate slug resolution: get_dashboard returns the dashboard with numeric ID\n client.get_dashboard = MagicMock(\n return_value={\n \"result\": {\"id\": 99, \"dashboard_title\": \"COVID Dashboard\", \"slug\": \"covid\"}\n }\n )\n client.extract_native_filters_from_key = MagicMock(\n return_value={\n \"dataMask\": {\"f_slug\": {}},\n \"dashboard_id\": 99,\n \"filter_state_key\": \"abc123\",\n }\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/superset/dashboard/covid/?native_filters_key=abc123\"\n )\n assert result[\"filter_type\"] == \"native_filters_key\"\n assert result[\"dashboard_id\"] == 99\n assert result[\"filters\"][\"dataMask\"][\"f_slug\"] == {}\n client.get_dashboard.assert_called_once_with(\"covid\")\n client.extract_native_filters_from_key.assert_called_once_with(99, \"abc123\")\n# #endregion test_parse_dashboard_url_for_filters_native_key_slug\n# #region test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Gracefully handle slug resolution failure for native_filters_key URL.\ndef test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails():\n client = SupersetClient(_make_environment())\n client.get_dashboard = MagicMock(side_effect=Exception(\"Not found\"))\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/unknownslug/?native_filters_key=key1\"\n )\n assert result[\"filter_type\"] is None\n assert result[\"dashboard_id\"] is None\n# #endregion test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails\n# #region test_parse_dashboard_url_for_filters_native_filters_direct [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters direct query param.\ndef test_parse_dashboard_url_for_filters_native_filters_direct():\n client = SupersetClient(_make_environment())\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/1/?native_filters=\"\n + json.dumps({\"filter_1\": {\"col\": \"x\", \"op\": \"==\", \"val\": \"y\"}})\n )\n assert result[\"filter_type\"] == \"native_filters\"\n assert \"dataMask\" in result[\"filters\"]\n# #endregion test_parse_dashboard_url_for_filters_native_filters_direct\n# #region test_parse_dashboard_url_for_filters_no_filters [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Return empty result when no filters present.\ndef test_parse_dashboard_url_for_filters_no_filters():\n client = SupersetClient(_make_environment())\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/1/\"\n )\n assert result[\"filter_type\"] is None\n assert result[\"filters\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_no_filters\n# #region test_extra_form_data_merge [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ExtraFormDataMerge correctly merges dictionaries.\ndef test_extra_form_data_merge():\n merger = ExtraFormDataMerge()\n original = {\n \"filters\": [{\"col\": \"a\", \"op\": \"IN\", \"val\": [1, 2]}],\n \"time_range\": \"2020-01-01 : 2021-01-01\",\n \"extras\": {\"where\": \"x > 0\"},\n }\n new = {\n \"filters\": [{\"col\": \"b\", \"op\": \"==\", \"val\": \"test\"}],\n \"time_range\": \"2022-01-01 : 2023-01-01\",\n \"columns\": [\"col1\", \"col2\"],\n }\n result = merger.merge(original, new)\n # Filters should be appended\n assert len(result[\"filters\"]) == 2\n assert result[\"filters\"][0][\"col\"] == \"a\"\n assert result[\"filters\"][1][\"col\"] == \"b\"\n # Time range should be overridden\n assert result[\"time_range\"] == \"2022-01-01 : 2023-01-01\"\n # Extras should remain\n assert result[\"extras\"] == {\"where\": \"x > 0\"}\n # New columns should be added\n assert result[\"columns\"] == [\"col1\", \"col2\"]\n# #endregion test_extra_form_data_merge\n# #region test_filter_state_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test FilterState Pydantic model.\ndef test_filter_state_model():\n state = FilterState(\n extraFormData={\"filters\": [{\"col\": \"x\", \"op\": \"==\", \"val\": \"y\"}]},\n filterState={\"value\": \"y\"},\n ownState={\"selectedValues\": [\"y\"]},\n )\n assert state.extraFormData[\"filters\"][0][\"col\"] == \"x\"\n assert state.filterState[\"value\"] == \"y\"\n assert state.ownState[\"selectedValues\"] == [\"y\"]\n# #endregion test_filter_state_model\n# #region test_parsed_native_filters_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ParsedNativeFilters Pydantic model.\ndef test_parsed_native_filters_model():\n filters = ParsedNativeFilters(\n dataMask={\"f1\": {\"extraFormData\": {}, \"filterState\": {}}},\n filter_type=\"permalink\",\n dashboard_id=\"42\",\n permalink_key=\"abc\",\n )\n assert filters.has_filters() is True\n assert filters.get_filter_count() == 1\n assert filters.filter_type == \"permalink\"\n# #endregion test_parsed_native_filters_model\n# #region test_parsed_native_filters_empty [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ParsedNativeFilters with no filters.\ndef test_parsed_native_filters_empty():\n filters = ParsedNativeFilters()\n assert filters.has_filters() is False\n assert filters.get_filter_count() == 0\n# #endregion test_parsed_native_filters_empty\n# #region test_native_filter_data_mask_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test NativeFilterDataMask model.\ndef test_native_filter_data_mask_model():\n data_mask = NativeFilterDataMask(\n filters={\n \"filter_1\": FilterState(extraFormData={\"filters\": []}, filterState={}),\n \"filter_2\": FilterState(\n extraFormData={\"time_range\": \"...\"}, filterState={}\n ),\n }\n )\n assert data_mask.get_filter_ids() == [\"filter_1\", \"filter_2\"]\n assert data_mask.get_extra_form_data(\"filter_1\") == {\"filters\": []}\n assert data_mask.get_extra_form_data(\"nonexistent\") == {}\n# #endregion test_native_filter_data_mask_model\n# #region test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Reconcile raw native filter ids from state to canonical metadata filter names.\ndef test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"normalized_value\": {\n \"filter_clauses\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 1\n assert result[0][\"filter_name\"] == \"Country\"\n assert result[0][\"display_name\"] == \"Country\"\n assert result[0][\"raw_value\"] == [\"DE\", \"FR\"]\n assert result[0][\"source\"] == \"superset_native_filters_key\"\n assert result[0][\"normalized_value\"] == {\n \"filter_clauses\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n }\n# #endregion test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names\n# #region test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Collapse raw-id state entries and metadata entries into one canonical filter.\ndef test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n },\n {\n \"id\": \"NATIVE_FILTER-vv123\",\n \"name\": \"Region\",\n \"label\": \"Region\",\n },\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 2\n country_filter = next(item for item in result if item[\"filter_name\"] == \"Country\")\n region_filter = next(item for item in result if item[\"filter_name\"] == \"Region\")\n assert country_filter[\"raw_value\"] == [\"DE\", \"FR\"]\n assert country_filter[\"recovery_status\"] == \"recovered\"\n assert region_filter[\"raw_value\"] is None\n assert region_filter[\"recovery_status\"] == \"partial\"\n# #endregion test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter\n# #region test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Preserve unmatched raw native filter ids as fallback diagnostics when metadata mapping is unavailable.\ndef test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"display_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"raw_value\": [\"orphan\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 2\n assert any(\n item[\"filter_name\"] == \"Country\" and item[\"recovery_status\"] == \"partial\"\n for item in result\n )\n assert any(\n item[\"filter_name\"] == \"UNKNOWN_NATIVE_FILTER\"\n and item[\"raw_value\"] == [\"orphan\"]\n and item[\"source\"] == \"superset_native_filters_key\"\n for item in result\n )\n# #endregion test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids\n# #region test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Recovered native filter state should preserve exact Superset clause payload and time extras for preview compilation.\ndef test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview():\n extractor = SupersetContextExtractor(_make_environment(), client=MagicMock())\n imported_filters = extractor._extract_imported_filters(\n {\n \"native_filter_state\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"NATIVE_FILTER-1\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n }\n }\n }\n )\n assert imported_filters == [\n {\n \"filter_name\": \"NATIVE_FILTER-1\",\n \"raw_value\": [\"DE\"],\n \"display_name\": \"Country\",\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ]\n# #endregion test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview\n# #region test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Assistant-facing imported-filter payload should redact likely PII while preserving structure metadata.\ndef test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values():\n result = sanitize_imported_filter_for_assistant(\n {\n \"filter_name\": \"customer_email\",\n \"raw_value\": [\"alice@example.com\", \"1234567890\"],\n \"normalized_value\": {\"filter_clauses\": []},\n \"source\": \"manual\",\n }\n )\n assert result[\"raw_value\"] == [\"***@example.com\", \"********90\"]\n assert result[\"raw_value_masked\"] is True\n assert result[\"normalized_value\"] == {\"filter_clauses\": []}\n# #endregion test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values\n# #endregion NativeFilterExtractionTests\n" }, { "contract_id": "_make_environment", @@ -18092,7 +18128,7 @@ "contract_type": "Module", "file_path": "backend/src/core/async_superset_client.py", "start_line": 1, - "end_line": 615, + "end_line": 614, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18104,14 +18140,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, transform, dashboard, filter, async-superset-client]\n# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously.\n# @LAYER Core\nimport asyncio\nimport json\nimport re\nfrom typing import Any, cast\n\nfrom .config_models import Environment\nfrom .logger import belief_scope, logger as app_logger\nfrom .superset_client import SupersetClient\nfrom .utils.async_network import AsyncAPIClient\n\n\n# #region AsyncSupersetClient [C:3] [TYPE Class]\n# @BRIEF Async sibling of SupersetClient for dashboard read paths.\n# @RELATION INHERITS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncAPIClient]\n# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\nclass AsyncSupersetClient(SupersetClient):\n # #region AsyncSupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE env is valid Environment instance.\n # @POST Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION DEPENDS_ON -> [AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n # #endregion AsyncSupersetClientInit\n # #region AsyncSupersetClientClose [TYPE Function] [C:3]\n # @PURPOSE: Close async transport resources.\n # @POST Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT Closes network sockets.\n # @RELATION CALLS -> [AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n # #endregion AsyncSupersetClientClose\n # #region get_dashboards_page_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST Returns total count and page result list.\n # @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion get_dashboards_page_async\n # #region get_dashboard_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_async\n # #region get_chart_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST Returns raw chart payload from Superset API.\n # @DATA_CONTRACT Input[chart_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(dict, response)\n # #endregion get_chart_async\n # #region get_dashboard_detail_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_async]\n # @RELATION CALLS -> [get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n app_logger.debug(\"Could not parse position JSON for dashboard %s\", dashboard_ref)\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n app_logger.debug(\"Could not parse json_metadata for dashboard %s\", dashboard_ref)\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n # #endregion get_dashboard_detail_async\n # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_permalink_state_async\n # #region get_native_filter_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST Returns native filter state payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion get_native_filter_state_async\n # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST Returns extracted dataMask with filter states.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion extract_native_filters_from_permalink_async\n # #region extract_native_filters_from_key_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion extract_native_filters_from_key_async\n # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT Input[url: str] -> Output[Dict]\n # @RELATION CALLS -> [extract_native_filters_from_permalink_async]\n # @RELATION CALLS -> [extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n app_logger.debug(\"Permalink key not found in URL for dashboard %s\", dashboard_ref)\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n app_logger.debug(\"Dashboard not found in path_parts for native_filters_key\")\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion parse_dashboard_url_for_filters_async\n# #endregion AsyncSupersetClient\n# #endregion AsyncSupersetClientModule\n" + "body": "# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, transform, dashboard, filter, async-superset-client]\n# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously.\n# @LAYER Core\nimport asyncio\nimport json\nimport re\nfrom typing import Any, cast\n\nfrom .config_models import Environment\nfrom .logger import belief_scope, logger as app_logger\nfrom .superset_client import SupersetClient\nfrom .utils.async_network import AsyncAPIClient\n\n\n# #region AsyncSupersetClient [C:3] [TYPE Class]\n# @BRIEF Async sibling of SupersetClient for dashboard read paths.\n# @RELATION INHERITS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncAPIClient]\nclass AsyncSupersetClient(SupersetClient):\n # #region AsyncSupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE env is valid Environment instance.\n # @POST Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION DEPENDS_ON -> [AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n # #endregion AsyncSupersetClientInit\n # #region AsyncSupersetClientClose [TYPE Function] [C:3]\n # @PURPOSE: Close async transport resources.\n # @POST Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT Closes network sockets.\n # @RELATION CALLS -> [AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n # #endregion AsyncSupersetClientClose\n # #region get_dashboards_page_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST Returns total count and page result list.\n # @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion get_dashboards_page_async\n # #region get_dashboard_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_async\n # #region get_chart_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST Returns raw chart payload from Superset API.\n # @DATA_CONTRACT Input[chart_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(dict, response)\n # #endregion get_chart_async\n # #region get_dashboard_detail_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_async]\n # @RELATION CALLS -> [get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n app_logger.debug(\"Could not parse position JSON for dashboard %s\", dashboard_ref)\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n app_logger.debug(\"Could not parse json_metadata for dashboard %s\", dashboard_ref)\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n # #endregion get_dashboard_detail_async\n # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_permalink_state_async\n # #region get_native_filter_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST Returns native filter state payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion get_native_filter_state_async\n # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST Returns extracted dataMask with filter states.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion extract_native_filters_from_permalink_async\n # #region extract_native_filters_from_key_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion extract_native_filters_from_key_async\n # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT Input[url: str] -> Output[Dict]\n # @RELATION CALLS -> [extract_native_filters_from_permalink_async]\n # @RELATION CALLS -> [extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n app_logger.debug(\"Permalink key not found in URL for dashboard %s\", dashboard_ref)\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n app_logger.debug(\"Dashboard not found in path_parts for native_filters_key\")\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion parse_dashboard_url_for_filters_async\n# #endregion AsyncSupersetClient\n# #endregion AsyncSupersetClientModule\n" }, { "contract_id": "AsyncSupersetClient", "contract_type": "Class", "file_path": "backend/src/core/async_superset_client.py", "start_line": 15, - "end_line": 614, + "end_line": 613, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18130,25 +18166,19 @@ "relation_type": "DEPENDS_ON", "target_id": "AsyncAPIClient", "target_ref": "[AsyncAPIClient]" - }, - { - "source_id": "AsyncSupersetClient", - "relation_type": "CALLS", - "target_id": "EXT:method:AsyncAPIClient.request", - "target_ref": "[EXT:method:AsyncAPIClient.request]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AsyncSupersetClient [C:3] [TYPE Class]\n# @BRIEF Async sibling of SupersetClient for dashboard read paths.\n# @RELATION INHERITS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncAPIClient]\n# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\nclass AsyncSupersetClient(SupersetClient):\n # #region AsyncSupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE env is valid Environment instance.\n # @POST Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION DEPENDS_ON -> [AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n # #endregion AsyncSupersetClientInit\n # #region AsyncSupersetClientClose [TYPE Function] [C:3]\n # @PURPOSE: Close async transport resources.\n # @POST Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT Closes network sockets.\n # @RELATION CALLS -> [AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n # #endregion AsyncSupersetClientClose\n # #region get_dashboards_page_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST Returns total count and page result list.\n # @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion get_dashboards_page_async\n # #region get_dashboard_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_async\n # #region get_chart_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST Returns raw chart payload from Superset API.\n # @DATA_CONTRACT Input[chart_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(dict, response)\n # #endregion get_chart_async\n # #region get_dashboard_detail_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_async]\n # @RELATION CALLS -> [get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n app_logger.debug(\"Could not parse position JSON for dashboard %s\", dashboard_ref)\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n app_logger.debug(\"Could not parse json_metadata for dashboard %s\", dashboard_ref)\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n # #endregion get_dashboard_detail_async\n # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_permalink_state_async\n # #region get_native_filter_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST Returns native filter state payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion get_native_filter_state_async\n # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST Returns extracted dataMask with filter states.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion extract_native_filters_from_permalink_async\n # #region extract_native_filters_from_key_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion extract_native_filters_from_key_async\n # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT Input[url: str] -> Output[Dict]\n # @RELATION CALLS -> [extract_native_filters_from_permalink_async]\n # @RELATION CALLS -> [extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n app_logger.debug(\"Permalink key not found in URL for dashboard %s\", dashboard_ref)\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n app_logger.debug(\"Dashboard not found in path_parts for native_filters_key\")\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion parse_dashboard_url_for_filters_async\n# #endregion AsyncSupersetClient\n" + "body": "# #region AsyncSupersetClient [C:3] [TYPE Class]\n# @BRIEF Async sibling of SupersetClient for dashboard read paths.\n# @RELATION INHERITS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncAPIClient]\nclass AsyncSupersetClient(SupersetClient):\n # #region AsyncSupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE env is valid Environment instance.\n # @POST Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION DEPENDS_ON -> [AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n # #endregion AsyncSupersetClientInit\n # #region AsyncSupersetClientClose [TYPE Function] [C:3]\n # @PURPOSE: Close async transport resources.\n # @POST Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT Closes network sockets.\n # @RELATION CALLS -> [AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n # #endregion AsyncSupersetClientClose\n # #region get_dashboards_page_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST Returns total count and page result list.\n # @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion get_dashboards_page_async\n # #region get_dashboard_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_async\n # #region get_chart_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST Returns raw chart payload from Superset API.\n # @DATA_CONTRACT Input[chart_id: int] -> Output[Dict]\n # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(dict, response)\n # #endregion get_chart_async\n # #region get_dashboard_detail_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_async]\n # @RELATION CALLS -> [get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n app_logger.debug(\"Could not parse position JSON for dashboard %s\", dashboard_ref)\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n app_logger.debug(\"Could not parse json_metadata for dashboard %s\", dashboard_ref)\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n # #endregion get_dashboard_detail_async\n # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_permalink_state_async\n # #region get_native_filter_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST Returns native filter state payload from Superset API.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion get_native_filter_state_async\n # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST Returns extracted dataMask with filter states.\n # @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion extract_native_filters_from_permalink_async\n # #region extract_native_filters_from_key_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION CALLS -> [get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion extract_native_filters_from_key_async\n # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT Input[url: str] -> Output[Dict]\n # @RELATION CALLS -> [extract_native_filters_from_permalink_async]\n # @RELATION CALLS -> [extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n app_logger.debug(\"Permalink key not found in URL for dashboard %s\", dashboard_ref)\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n app_logger.debug(\"Dashboard not found in path_parts for native_filters_key\")\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion parse_dashboard_url_for_filters_async\n# #endregion AsyncSupersetClient\n" }, { "contract_id": "AsyncSupersetClientInit", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 21, - "end_line": 41, + "start_line": 20, + "end_line": 40, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18175,8 +18205,8 @@ "contract_id": "AsyncSupersetClientClose", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 42, - "end_line": 49, + "start_line": 41, + "end_line": 48, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18202,8 +18232,8 @@ "contract_id": "get_dashboards_page_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 50, - "end_line": 84, + "start_line": 49, + "end_line": 83, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18229,8 +18259,8 @@ "contract_id": "get_dashboard_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 85, - "end_line": 98, + "start_line": 84, + "end_line": 97, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18256,8 +18286,8 @@ "contract_id": "get_chart_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 99, - "end_line": 110, + "start_line": 98, + "end_line": 109, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18283,8 +18313,8 @@ "contract_id": "get_dashboard_detail_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 111, - "end_line": 395, + "start_line": 110, + "end_line": 394, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18316,8 +18346,8 @@ "contract_id": "get_dashboard_permalink_state_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 396, - "end_line": 409, + "start_line": 395, + "end_line": 408, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -18336,8 +18366,8 @@ "contract_id": "get_native_filter_state_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 410, - "end_line": 426, + "start_line": 409, + "end_line": 425, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -18356,8 +18386,8 @@ "contract_id": "extract_native_filters_from_permalink_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 427, - "end_line": 461, + "start_line": 426, + "end_line": 460, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18383,8 +18413,8 @@ "contract_id": "extract_native_filters_from_key_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 462, - "end_line": 514, + "start_line": 461, + "end_line": 513, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18410,8 +18440,8 @@ "contract_id": "parse_dashboard_url_for_filters_async", "contract_type": "Function", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 515, - "end_line": 613, + "start_line": 514, + "end_line": 612, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -18456,6 +18486,32 @@ "tier_source": "AutoCalculated", "body": "# #region AuthPackage [TYPE Package] [SEMANTICS auth, package, init]\n# @BRIEF Authentication and authorization package root.\n# #endregion AuthPackage\n" }, + { + "contract_id": "test_auth", + "contract_type": "Module", + "file_path": "backend/src/core/auth/__tests__/test_auth.py", + "start_line": 1, + "end_line": 259, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Unit tests for authentication module", + "COMPLEXITY": 3, + "LAYER": "Domain" + }, + "relations": [ + { + "source_id": "test_auth", + "relation_type": "BINDS_TO", + "target_id": "AuthPackage", + "target_ref": "AuthPackage" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region test_auth [TYPE Module] [C:3] [SEMANTICS test, auth, authentication, unit]\n# @BRIEF Unit tests for authentication module\n# @LAYER Domain\n# @RELATION BINDS_TO -> AuthPackage\nfrom pathlib import Path\nimport sys\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent.parent / \"src\"))\nimport pytest\n\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\n\n# Import all models to ensure they are registered with Base before create_all - must import both auth and mapping to ensure Base knows about all tables\nfrom src.models.auth import ADGroupMapping, Permission, Role, User\nfrom src.services.auth_service import AuthService\n\n# Create in-memory SQLite database for testing\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(\n SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False}\n)\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# Create all tables\nBase.metadata.create_all(bind=engine)\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 yield session\n session.close()\n transaction.rollback()\n connection.close()\n@pytest.fixture\ndef auth_service(db_session):\n return AuthService(db_session)\n@pytest.fixture\ndef auth_repo(db_session):\n return AuthRepository(db_session)\n# #region test_create_user [TYPE Function]\n# @BRIEF Verifies that a persisted user can be retrieved with intact credential hash.\n# @RELATION BINDS_TO -> [test_auth]\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 auth_repo.db.add(user)\n auth_repo.db.commit()\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# #endregion test_create_user\n# #region test_authenticate_user [TYPE Function]\n# @BRIEF Validates authentication outcomes for valid, wrong-password, and unknown-user cases.\n# @RELATION BINDS_TO -> [test_auth]\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 auth_repo.db.add(user)\n auth_repo.db.commit()\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 # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n# #endregion test_authenticate_user\n# #region test_create_session [TYPE Function]\n# @BRIEF Ensures session creation returns bearer token payload fields.\n# @RELATION BINDS_TO -> [test_auth]\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 auth_repo.db.add(user)\n auth_repo.db.commit()\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# #endregion test_create_session\n# #region test_role_permission_association [TYPE Function]\n# @BRIEF Confirms role-permission many-to-many assignments persist and reload correctly.\n# @RELATION BINDS_TO -> [test_auth]\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 role.permissions.extend([perm1, perm2])\n auth_repo.db.add(role)\n auth_repo.db.commit()\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 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# #endregion test_role_permission_association\n# #region test_user_role_association [TYPE Function]\n# @BRIEF Confirms user-role assignment persists and is queryable from repository reads.\n# @RELATION BINDS_TO -> [test_auth]\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 user.roles.append(role)\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\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# #endregion test_user_role_association\n# #region test_ad_group_mapping [TYPE Function]\n# @BRIEF Verifies AD group mapping rows persist and reference the expected role.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n auth_repo.db.add(role)\n auth_repo.db.commit()\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\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# #endregion test_ad_group_mapping\n# #region test_authenticate_user_updates_last_login [TYPE Function]\n# @BRIEF Verifies successful authentication updates last_login audit field.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_authenticate_user_updates_last_login(auth_service, auth_repo):\n \"\"\"@SIDE_EFFECT: authenticate_user updates last_login timestamp on success.\"\"\"\n user = User(\n username=\"loginuser\",\n email=\"login@example.com\",\n password_hash=get_password_hash(\"mypassword\"),\n auth_source=\"LOCAL\",\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n assert user.last_login is None\n authenticated = auth_service.authenticate_user(\"loginuser\", \"mypassword\")\n assert authenticated is not None\n assert authenticated.last_login is not None\n# #endregion test_authenticate_user_updates_last_login\n# #region test_authenticate_inactive_user [TYPE Function]\n# @BRIEF Verifies inactive accounts are rejected during password authentication.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_authenticate_inactive_user(auth_service, auth_repo):\n \"\"\"@PRE: User with is_active=False should not authenticate.\"\"\"\n user = User(\n username=\"inactive_user\",\n email=\"inactive@example.com\",\n password_hash=get_password_hash(\"testpass\"),\n auth_source=\"LOCAL\",\n is_active=False,\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n result = auth_service.authenticate_user(\"inactive_user\", \"testpass\")\n assert result is None\n# #endregion test_authenticate_inactive_user\n# #region test_verify_password_empty_hash [TYPE Function]\n# @BRIEF Verifies password verification safely rejects empty or null password hashes.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_verify_password_empty_hash():\n \"\"\"@PRE: verify_password with empty/None hash returns False.\"\"\"\n assert verify_password(\"anypassword\", \"\") is False\n assert verify_password(\"anypassword\", None) is False\n# #endregion test_verify_password_empty_hash\n# #region test_provision_adfs_user_new [TYPE Function]\n# @BRIEF Verifies JIT provisioning creates a new ADFS user and maps group-derived roles.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_provision_adfs_user_new(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user creates a new ADFS user with correct roles.\"\"\"\n # Set up a role and AD group mapping\n role = Role(name=\"ADFS_Viewer\", description=\"ADFS viewer role\")\n auth_repo.db.add(role)\n auth_repo.db.commit()\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\Viewers\", role_id=role.id)\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n user_info = {\n \"upn\": \"newadfsuser@domain.com\",\n \"email\": \"newadfsuser@domain.com\",\n \"groups\": [\"DOMAIN\\\\Viewers\"],\n }\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"newadfsuser@domain.com\"\n assert user.auth_source == \"ADFS\"\n assert user.is_active is True\n assert len(user.roles) == 1\n assert user.roles[0].name == \"ADFS_Viewer\"\n# #endregion test_provision_adfs_user_new\n# #region test_provision_adfs_user_existing [TYPE Function]\n# @BRIEF Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_provision_adfs_user_existing(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user updates roles for existing user.\"\"\"\n # Create existing user\n existing = User(\n username=\"existingadfs@domain.com\",\n email=\"existingadfs@domain.com\",\n auth_source=\"ADFS\",\n is_active=True,\n )\n auth_repo.db.add(existing)\n auth_repo.db.commit()\n user_info = {\n \"upn\": \"existingadfs@domain.com\",\n \"email\": \"existingadfs@domain.com\",\n \"groups\": [],\n }\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"existingadfs@domain.com\"\n assert len(user.roles) == 0 # No matching group mappings\n# #endregion test_provision_adfs_user_existing\n# #endregion test_auth\n" + }, { "contract_id": "test_create_user", "contract_type": "Function", @@ -18701,7 +18757,7 @@ "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", "start_line": 235, - "end_line": 259, + "end_line": 258, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -18718,7 +18774,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_provision_adfs_user_existing [TYPE Function]\n# @BRIEF Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_provision_adfs_user_existing(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user updates roles for existing user.\"\"\"\n # Create existing user\n existing = User(\n username=\"existingadfs@domain.com\",\n email=\"existingadfs@domain.com\",\n auth_source=\"ADFS\",\n is_active=True,\n )\n auth_repo.db.add(existing)\n auth_repo.db.commit()\n user_info = {\n \"upn\": \"existingadfs@domain.com\",\n \"email\": \"existingadfs@domain.com\",\n \"groups\": [],\n }\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"existingadfs@domain.com\"\n assert len(user.roles) == 0 # No matching group mappings\n# #endregion test_auth\n# #endregion test_provision_adfs_user_existing\n" + "body": "# #region test_provision_adfs_user_existing [TYPE Function]\n# @BRIEF Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.\n# @RELATION BINDS_TO -> [test_auth]\ndef test_provision_adfs_user_existing(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user updates roles for existing user.\"\"\"\n # Create existing user\n existing = User(\n username=\"existingadfs@domain.com\",\n email=\"existingadfs@domain.com\",\n auth_source=\"ADFS\",\n is_active=True,\n )\n auth_repo.db.add(existing)\n auth_repo.db.commit()\n user_info = {\n \"upn\": \"existingadfs@domain.com\",\n \"email\": \"existingadfs@domain.com\",\n \"groups\": [],\n }\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"existingadfs@domain.com\"\n assert len(user.roles) == 0 # No matching group mappings\n# #endregion test_provision_adfs_user_existing\n" }, { "contract_id": "APIKeyUtilities", @@ -19951,7 +20007,7 @@ "contract_type": "Module", "file_path": "backend/src/core/config_models.py", "start_line": 1, - "end_line": 143, + "end_line": 150, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -19963,27 +20019,63 @@ { "source_id": "ConfigModels", "relation_type": "IMPLEMENTS", - "target_id": "EXT:internal:CoreContracts", - "target_ref": "[EXT:internal:CoreContracts]" + "target_id": "CoreContracts", + "target_ref": "[CoreContracts]" }, { "source_id": "ConfigModels", "relation_type": "IMPLEMENTS", - "target_id": "EXT:internal:ConnectionContracts", - "target_ref": "[EXT:internal:ConnectionContracts]" + "target_id": "ConnectionContracts", + "target_ref": "[ConnectionContracts]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ConfigModels [C:3] [TYPE Module] [SEMANTICS pydantic, model, schedule]\n# @BRIEF Defines the data models for application configuration using Pydantic.\n# @LAYER Core\n# @RELATION IMPLEMENTS -> [EXT:internal:CoreContracts]\n# @RELATION IMPLEMENTS -> [EXT:internal:ConnectionContracts]\n\n\nfrom pydantic import BaseModel, Field, field_validator\n\nfrom ..models.storage import StorageConfig\nfrom ..services.llm_prompt_templates import (\n DEFAULT_LLM_ASSISTANT_SETTINGS,\n DEFAULT_LLM_PROMPTS,\n DEFAULT_LLM_PROVIDER_BINDINGS,\n)\n\n\n# #region Schedule [TYPE DataClass]\n# @BRIEF Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# #endregion Schedule\n\n\n# #region Environment [TYPE DataClass]\n# @BRIEF Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# #endregion Environment\n\n\n# #region LoggingConfig [TYPE DataClass]\n# @BRIEF Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: str | None = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# #endregion LoggingConfig\n\n\n# #region CleanReleaseConfig [TYPE DataClass]\n# @BRIEF Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: str | None = None\n active_registry_id: str | None = None\n\n\n# #endregion CleanReleaseConfig\n\n\n# #region FeaturesConfig [C:1] [TYPE DataClass]\n# @BRIEF Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# #endregion FeaturesConfig\n\n\n# #region GlobalSettings [TYPE DataClass]\n# @BRIEF Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: str | None = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: list[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Application timezone\n app_timezone: str = \"Europe/Moscow\"\n\n @field_validator(\"app_timezone\")\n @classmethod\n def validate_app_timezone(cls, v: str) -> str:\n from zoneinfo import ZoneInfo\n try:\n ZoneInfo(v)\n except (KeyError, TypeError):\n raise ValueError(f\"Invalid IANA timezone: {v}\") from None\n return v\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# #endregion GlobalSettings\n\n\n# #region AppConfig [TYPE DataClass]\n# @BRIEF The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: list[Environment] = []\n settings: GlobalSettings\n\n\n# #endregion AppConfig\n\n# #endregion ConfigModels\n" + "body": "# #region ConfigModels [C:3] [TYPE Module] [SEMANTICS pydantic, model, schedule]\n# @BRIEF Defines the data models for application configuration using Pydantic.\n# @LAYER Core\n# @RELATION IMPLEMENTS -> [CoreContracts]\n# @RELATION IMPLEMENTS -> [ConnectionContracts]\n#\n# #region CoreContracts [C:2] [TYPE Interface]\n# @BRIEF Contract for core configuration models — Pydantic BaseModel subclasses with scheduler config.\n# #endregion CoreContracts\n#\n# #region ConnectionContracts [C:2] [TYPE Interface]\n# @BRIEF Contract for database/environment connection configuration models.\n# #endregion ConnectionContracts\n\nfrom pydantic import BaseModel, Field, field_validator\n\nfrom ..models.storage import StorageConfig\nfrom ..services.llm_prompt_templates import (\n DEFAULT_LLM_ASSISTANT_SETTINGS,\n DEFAULT_LLM_PROMPTS,\n DEFAULT_LLM_PROVIDER_BINDINGS,\n)\n\n\n# #region Schedule [TYPE DataClass]\n# @BRIEF Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# #endregion Schedule\n\n\n# #region Environment [TYPE DataClass]\n# @BRIEF Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# #endregion Environment\n\n\n# #region LoggingConfig [TYPE DataClass]\n# @BRIEF Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: str | None = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# #endregion LoggingConfig\n\n\n# #region CleanReleaseConfig [TYPE DataClass]\n# @BRIEF Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: str | None = None\n active_registry_id: str | None = None\n\n\n# #endregion CleanReleaseConfig\n\n\n# #region FeaturesConfig [C:1] [TYPE DataClass]\n# @BRIEF Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# #endregion FeaturesConfig\n\n\n# #region GlobalSettings [TYPE DataClass]\n# @BRIEF Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: str | None = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: list[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Application timezone\n app_timezone: str = \"Europe/Moscow\"\n\n @field_validator(\"app_timezone\")\n @classmethod\n def validate_app_timezone(cls, v: str) -> str:\n from zoneinfo import ZoneInfo\n try:\n ZoneInfo(v)\n except (KeyError, TypeError):\n raise ValueError(f\"Invalid IANA timezone: {v}\") from None\n return v\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# #endregion GlobalSettings\n\n\n# #region AppConfig [TYPE DataClass]\n# @BRIEF The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: list[Environment] = []\n settings: GlobalSettings\n\n\n# #endregion AppConfig\n\n# #endregion ConfigModels\n" + }, + { + "contract_id": "CoreContracts", + "contract_type": "Interface", + "file_path": "backend/src/core/config_models.py", + "start_line": 7, + "end_line": 9, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Contract for core configuration models — Pydantic BaseModel subclasses with scheduler config.", + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region CoreContracts [C:2] [TYPE Interface]\n# @BRIEF Contract for core configuration models — Pydantic BaseModel subclasses with scheduler config.\n# #endregion CoreContracts\n" + }, + { + "contract_id": "ConnectionContracts", + "contract_type": "Interface", + "file_path": "backend/src/core/config_models.py", + "start_line": 11, + "end_line": 13, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Contract for database/environment connection configuration models.", + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region ConnectionContracts [C:2] [TYPE Interface]\n# @BRIEF Contract for database/environment connection configuration models.\n# #endregion ConnectionContracts\n" }, { "contract_id": "Schedule", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 18, - "end_line": 25, + "start_line": 25, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19999,8 +20091,8 @@ "contract_id": "Environment", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 28, - "end_line": 44, + "start_line": 35, + "end_line": 51, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20016,8 +20108,8 @@ "contract_id": "LoggingConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 47, - "end_line": 60, + "start_line": 54, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20033,8 +20125,8 @@ "contract_id": "CleanReleaseConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 63, - "end_line": 70, + "start_line": 70, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20050,8 +20142,8 @@ "contract_id": "FeaturesConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 73, - "end_line": 82, + "start_line": 80, + "end_line": 89, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20069,8 +20161,8 @@ "contract_id": "GlobalSettings", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 85, - "end_line": 131, + "start_line": 92, + "end_line": 138, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20086,8 +20178,8 @@ "contract_id": "AppConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 134, - "end_line": 141, + "start_line": 141, + "end_line": 148, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20115,7 +20207,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CotLoggerModule [C:4] [TYPE Module] [SEMANTICS logging, cot, json, trace, structured]\n# @BRIEF Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol.\n# Uses ContextVar for trace_id and span_id propagation across async contexts.\n# Provides log(), MarkerLogger, seed_trace_id(), push_span(), pop_span().\n# @LAYER Core\n# @RELATION CALLED_BY -> [TraceContextMiddleware]\n# @RELATION CALLED_BY -> [EXT:internal:All C4+ service and route modules]\n# @PRE Python 3.7+ (ContextVar available).\n# @POST JSON log records written to the 'cot' logger at appropriate levels.\n# @SIDE_EFFECT Writes structured JSON to the 'cot' Python logger.\n# @DATA_CONTRACT Log call -> Single-line JSON to logging.StreamHandler/file.\n\nfrom contextvars import ContextVar\nimport logging\nfrom typing import Any\nimport uuid\n\n# #region cot_trace_context [C:1] [TYPE Data] [SEMANTICS contextvar, trace_id, span_id, propagation]\n# @BRIEF ContextVars for trace ID and span ID propagation across async boundaries.\n_trace_id: ContextVar[str] = ContextVar(\"_trace_id\", default=\"\")\n_span_id: ContextVar[str] = ContextVar(\"_span_id\", default=\"\")\n# #endregion cot_trace_context\n\n# #region cot_logger_instance [C:1] [TYPE Data] [SEMANTICS logger, instance]\n# @BRIEF Dedicated Python logger for all CoT (molecular) log output.\ncot_logger = logging.getLogger(\"cot\")\n# #endregion cot_logger_instance\n\n__all__ = [\n \"MarkerLogger\",\n \"get_trace_id\",\n \"log\",\n \"pop_span\",\n \"push_span\",\n \"seed_trace_id\",\n \"set_trace_id\",\n]\n\n# #region seed_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, uuid, contextvar, set]\n# @BRIEF Generate a new UUID4 trace_id, set it in ContextVar, and return it.\ndef seed_trace_id() -> str:\n \"\"\"Generate a new UUID4 trace ID and store it in the thread-local ContextVar.\n\n Returns:\n str: The newly generated trace ID.\n \"\"\"\n trace_id = str(uuid.uuid4())\n _trace_id.set(trace_id)\n return trace_id\n\n\n# #endregion seed_trace_id\n\n# #region set_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, set, public]\n# @BRIEF Set an explicit trace_id into the ContextVar (e.g. from an incoming header).\ndef set_trace_id(trace_id: str) -> None:\n \"\"\"Set a specific trace_id into the ContextVar.\n\n This is used by TraceContextMiddleware when an ``X-Trace-ID`` header\n is present, enabling cross-service trace chaining.\n\n Args:\n trace_id: The trace ID to set.\n \"\"\"\n _trace_id.set(trace_id)\n\n\n# #endregion set_trace_id\n\n# #region get_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, get, public]\n# @BRIEF Get the current trace_id from ContextVar.\ndef get_trace_id() -> str:\n \"\"\"Get the current trace_id from the thread-local ContextVar.\n\n Returns:\n str: The current trace ID, or empty string if none set.\n \"\"\"\n return _trace_id.get()\n\n\n# #endregion get_trace_id\n\n# #region push_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, stack]\n# @BRIEF Set a new span_id in ContextVar and return the previous span_id for later restoration.\ndef push_span(span: str) -> str:\n \"\"\"Push a new span ID onto the context and return the previous span ID.\n\n Args:\n span: The new span identifier (e.g. function or operation name).\n\n Returns:\n str: The previous span ID, suitable for passing to pop_span().\n \"\"\"\n prev = _span_id.get()\n _span_id.set(span)\n return prev\n\n\n# #endregion push_span\n\n# #region pop_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, restore]\n# @BRIEF Restore a previous span_id into the ContextVar.\ndef pop_span(prev: str) -> None:\n \"\"\"Restore a previous span ID into the ContextVar.\n\n Args:\n prev: The span ID to restore (previously returned by push_span).\n \"\"\"\n _span_id.set(prev)\n\n\n# #endregion pop_span\n\n# #region cot_log_function [C:2] [TYPE Function] [SEMANTICS log, json, structured, marker]\n# @BRIEF Core structured logging function that emits a single-line JSON record.\ndef log(\n src: str,\n marker: str,\n intent: str,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n level: str | None = None,\n) -> None:\n \"\"\"Emit a single-line structured JSON log record through the 'cot' logger.\n\n Args:\n src: Qualified function or component name (e.g. 'AuthRepository.get_user').\n marker: Protocol marker — one of 'REASON', 'REFLECT', 'EXPLORE'.\n intent: Short one-line description of the intent or action.\n payload: Optional structured data dict to include in the record.\n error: Required for EXPLORE markers; describes the violated assumption.\n level: Log level override. Auto-inferred from marker if omitted\n (REASON/REFLECT -> INFO, EXPLORE -> WARNING).\n\n Side effects:\n Writes a single-line JSON string to the 'cot' Python logger.\n \"\"\"\n if level is None:\n level = \"WARNING\" if marker == \"EXPLORE\" else \"INFO\"\n\n # Build structured extra data for the LogRecord.\n # CotJsonFormatter will read these attributes to produce the final JSON output,\n # consistent with the main superset_tools_app logger.\n extra: dict[str, Any] = {\n \"marker\": marker,\n \"intent\": intent,\n \"src\": src,\n }\n\n if payload is not None:\n extra[\"payload\"] = payload\n\n if error is not None:\n extra[\"error\"] = error\n\n log_func = {\n \"WARNING\": cot_logger.warning,\n \"ERROR\": cot_logger.error,\n \"DEBUG\": cot_logger.debug,\n }.get(level, cot_logger.info)\n\n log_func(intent, extra=extra)\n\n\n# #endregion cot_log_function\n\n# #region MarkerLogger [C:2] [TYPE Class] [SEMANTICS logger, proxy, marker, syntactic-sugar]\n# @BRIEF Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.\nclass MarkerLogger:\n \"\"\"Thin proxy over the cot_logger.log() function.\n\n Usage::\n\n log = MarkerLogger(\"AuthService\")\n log.reason(\"Validating credentials\", payload={\"user_id\": \"...\"})\n log.reflect(\"Validation complete\", payload={\"result\": \"ok\"})\n log.explore(\"Token expired\", error=\"JWT expired, attempting refresh\")\n\n Each method delegates to :func:`log` with the corresponding marker.\n \"\"\"\n\n # #region MarkerLogger.__init__ [TYPE Function]\n # @BRIEF Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # #endregion MarkerLogger.__init__\n\n # #region MarkerLogger.reason [TYPE Function]\n # @BRIEF Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # #endregion MarkerLogger.reason\n\n # #region MarkerLogger.reflect [TYPE Function]\n # @BRIEF Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # #endregion MarkerLogger.reflect\n\n # #region MarkerLogger.explore [TYPE Function]\n # @BRIEF Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # #endregion MarkerLogger.explore\n\n\n# #endregion MarkerLogger\n# #endregion CotLoggerModule\n" + "body": "# #region CotLoggerModule [C:4] [TYPE Module] [SEMANTICS logging, cot, json, trace, structured]\n# @BRIEF Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol.\n# Uses ContextVar for trace_id and span_id propagation across async contexts.\n# Provides log(), MarkerLogger, seed_trace_id(), push_span(), pop_span().\n# @LAYER Core\n# @RELATION CALLED_BY -> [TraceContextMiddleware]\n# @RELATION CALLED_BY -> [CotLoggerModule]\n# @PRE Python 3.7+ (ContextVar available).\n# @POST JSON log records written to the 'cot' logger at appropriate levels.\n# @SIDE_EFFECT Writes structured JSON to the 'cot' Python logger.\n# @DATA_CONTRACT Log call -> Single-line JSON to logging.StreamHandler/file.\n\nfrom contextvars import ContextVar\nimport logging\nfrom typing import Any\nimport uuid\n\n# #region cot_trace_context [C:1] [TYPE Data] [SEMANTICS contextvar, trace_id, span_id, propagation]\n# @BRIEF ContextVars for trace ID and span ID propagation across async boundaries.\n_trace_id: ContextVar[str] = ContextVar(\"_trace_id\", default=\"\")\n_span_id: ContextVar[str] = ContextVar(\"_span_id\", default=\"\")\n# #endregion cot_trace_context\n\n# #region cot_logger_instance [C:1] [TYPE Data] [SEMANTICS logger, instance]\n# @BRIEF Dedicated Python logger for all CoT (molecular) log output.\ncot_logger = logging.getLogger(\"cot\")\n# #endregion cot_logger_instance\n\n__all__ = [\n \"MarkerLogger\",\n \"get_trace_id\",\n \"log\",\n \"pop_span\",\n \"push_span\",\n \"seed_trace_id\",\n \"set_trace_id\",\n]\n\n# #region seed_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, uuid, contextvar, set]\n# @BRIEF Generate a new UUID4 trace_id, set it in ContextVar, and return it.\ndef seed_trace_id() -> str:\n \"\"\"Generate a new UUID4 trace ID and store it in the thread-local ContextVar.\n\n Returns:\n str: The newly generated trace ID.\n \"\"\"\n trace_id = str(uuid.uuid4())\n _trace_id.set(trace_id)\n return trace_id\n\n\n# #endregion seed_trace_id\n\n# #region set_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, set, public]\n# @BRIEF Set an explicit trace_id into the ContextVar (e.g. from an incoming header).\ndef set_trace_id(trace_id: str) -> None:\n \"\"\"Set a specific trace_id into the ContextVar.\n\n This is used by TraceContextMiddleware when an ``X-Trace-ID`` header\n is present, enabling cross-service trace chaining.\n\n Args:\n trace_id: The trace ID to set.\n \"\"\"\n _trace_id.set(trace_id)\n\n\n# #endregion set_trace_id\n\n# #region get_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, get, public]\n# @BRIEF Get the current trace_id from ContextVar.\ndef get_trace_id() -> str:\n \"\"\"Get the current trace_id from the thread-local ContextVar.\n\n Returns:\n str: The current trace ID, or empty string if none set.\n \"\"\"\n return _trace_id.get()\n\n\n# #endregion get_trace_id\n\n# #region push_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, stack]\n# @BRIEF Set a new span_id in ContextVar and return the previous span_id for later restoration.\ndef push_span(span: str) -> str:\n \"\"\"Push a new span ID onto the context and return the previous span ID.\n\n Args:\n span: The new span identifier (e.g. function or operation name).\n\n Returns:\n str: The previous span ID, suitable for passing to pop_span().\n \"\"\"\n prev = _span_id.get()\n _span_id.set(span)\n return prev\n\n\n# #endregion push_span\n\n# #region pop_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, restore]\n# @BRIEF Restore a previous span_id into the ContextVar.\ndef pop_span(prev: str) -> None:\n \"\"\"Restore a previous span ID into the ContextVar.\n\n Args:\n prev: The span ID to restore (previously returned by push_span).\n \"\"\"\n _span_id.set(prev)\n\n\n# #endregion pop_span\n\n# #region cot_log_function [C:2] [TYPE Function] [SEMANTICS log, json, structured, marker]\n# @BRIEF Core structured logging function that emits a single-line JSON record.\ndef log(\n src: str,\n marker: str,\n intent: str,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n level: str | None = None,\n) -> None:\n \"\"\"Emit a single-line structured JSON log record through the 'cot' logger.\n\n Args:\n src: Qualified function or component name (e.g. 'AuthRepository.get_user').\n marker: Protocol marker — one of 'REASON', 'REFLECT', 'EXPLORE'.\n intent: Short one-line description of the intent or action.\n payload: Optional structured data dict to include in the record.\n error: Required for EXPLORE markers; describes the violated assumption.\n level: Log level override. Auto-inferred from marker if omitted\n (REASON/REFLECT -> INFO, EXPLORE -> WARNING).\n\n Side effects:\n Writes a single-line JSON string to the 'cot' Python logger.\n \"\"\"\n if level is None:\n level = \"WARNING\" if marker == \"EXPLORE\" else \"INFO\"\n\n # Build structured extra data for the LogRecord.\n # CotJsonFormatter will read these attributes to produce the final JSON output,\n # consistent with the main superset_tools_app logger.\n extra: dict[str, Any] = {\n \"marker\": marker,\n \"intent\": intent,\n \"src\": src,\n }\n\n if payload is not None:\n extra[\"payload\"] = payload\n\n if error is not None:\n extra[\"error\"] = error\n\n log_func = {\n \"WARNING\": cot_logger.warning,\n \"ERROR\": cot_logger.error,\n \"DEBUG\": cot_logger.debug,\n }.get(level, cot_logger.info)\n\n log_func(intent, extra=extra)\n\n\n# #endregion cot_log_function\n\n# #region MarkerLogger [C:2] [TYPE Class] [SEMANTICS logger, proxy, marker, syntactic-sugar]\n# @BRIEF Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.\nclass MarkerLogger:\n \"\"\"Thin proxy over the cot_logger.log() function.\n\n Usage::\n\n log = MarkerLogger(\"AuthService\")\n log.reason(\"Validating credentials\", payload={\"user_id\": \"...\"})\n log.reflect(\"Validation complete\", payload={\"result\": \"ok\"})\n log.explore(\"Token expired\", error=\"JWT expired, attempting refresh\")\n\n Each method delegates to :func:`log` with the corresponding marker.\n \"\"\"\n\n # #region MarkerLogger.__init__ [TYPE Function]\n # @BRIEF Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # #endregion MarkerLogger.__init__\n\n # #region MarkerLogger.reason [TYPE Function]\n # @BRIEF Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # #endregion MarkerLogger.reason\n\n # #region MarkerLogger.reflect [TYPE Function]\n # @BRIEF Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # #endregion MarkerLogger.reflect\n\n # #region MarkerLogger.explore [TYPE Function]\n # @BRIEF Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # #endregion MarkerLogger.explore\n\n\n# #endregion MarkerLogger\n# #endregion CotLoggerModule\n" }, { "contract_id": "cot_trace_context", @@ -21007,8 +21099,8 @@ { "source_id": "LoggerModule", "relation_type": "CALLED_BY", - "target_id": "EXT:internal:All application modules", - "target_ref": "[EXT:internal:All application modules]" + "target_id": "LoggerModule", + "target_ref": "[LoggerModule]" }, { "source_id": "LoggerModule", @@ -21026,7 +21118,7 @@ "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 -> [EXT:internal:All application modules]\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 -> [EXT:internal:CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n # @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# @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" }, { "contract_id": "CotJsonFormatter", @@ -21044,14 +21136,14 @@ { "source_id": "CotJsonFormatter", "relation_type": "IMPLEMENTS", - "target_id": "EXT:internal:CotJsonFormat", - "target_ref": "[EXT:internal:CotJsonFormat]" + "target_id": "CotJsonFormatter", + "target_ref": "[CotJsonFormatter]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [EXT:internal:CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n # @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" + "body": "# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [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" }, { "contract_id": "CotJsonFormatter.format", @@ -21306,14 +21398,14 @@ { "source_id": "test_logger", "relation_type": "BINDS_TO", - "target_id": "EXT:path:src.core.logger", - "target_ref": "[EXT:path:src.core.logger]" + "target_id": "LoggerModule", + "target_ref": "[LoggerModule]" } ], "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 -> [EXT:path:src.core.logger]\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\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" }, { "contract_id": "test_belief_scope_logs_reason_reflect_at_debug", @@ -22447,7 +22539,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MigrationEngineModule [C:5] [TYPE Module] [SEMANTICS migration, superset, archive, migration-engine]\n#\n# @BRIEF Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION DEPENDS_ON -> [ResourceType]\n# @RELATION DEPENDS_ON -> [EXT:Library:yaml]\n# @PRE Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs.\n# @POST Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines.\n# @SIDE_EFFECT Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs.\n# @DATA_CONTRACT Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive]\n# @INVARIANT ZIP structure and non-targeted metadata must remain valid after transformation.\nimport json\nimport os\nfrom pathlib import Path\nimport re\nimport tempfile\nimport zipfile\n\nimport yaml\n\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import ResourceType\n\nfrom .logger import belief_scope, logger\n\n\n# #region MigrationEngine [TYPE Class]\n# @BRIEF Engine for transforming Superset export ZIPs.\n# @RELATION DEPENDS_ON -> [[EXT:list:MigrationEngine_internal_methods]]\nclass MigrationEngine:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: IdMappingService | None = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n # #endregion __init__\n # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION DEPENDS_ON -> [[EXT:list:MigrateEngine_transform_methods]]\n # @PARAM zip_path (str) - Path to the source ZIP file.\n # @PARAM output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM file_path (Path) - Path to the YAML file.\n # @PARAM db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT Input[Path] -> Output[Dict[int,str]]\n # @PARAM temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n logger.debug(\"Could not parse chart YAML file %s\", cf)\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM file_path (Path)\n # @PARAM target_env_id (str)\n # @PARAM source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n# #endregion MigrationEngine\n# #endregion MigrationEngineModule\n" + "body": "# #region MigrationEngineModule [C:5] [TYPE Module] [SEMANTICS migration, superset, archive, migration-engine]\n#\n# @BRIEF Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION DEPENDS_ON -> [ResourceType]\n# @RELATION DEPENDS_ON -> [EXT:Library:yaml]\n# @PRE Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs.\n# @POST Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines.\n# @SIDE_EFFECT Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs.\n# @DATA_CONTRACT Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive]\n# @INVARIANT ZIP structure and non-targeted metadata must remain valid after transformation.\nimport json\nimport os\nfrom pathlib import Path\nimport re\nimport tempfile\nimport zipfile\n\nimport yaml\n\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import ResourceType\n\nfrom .logger import belief_scope, logger\n\n\n# #region MigrationEngine [TYPE Class]\n# @BRIEF Engine for transforming Superset export ZIPs.\n# @RELATION DEPENDS_ON -> [MigrationEngine]\nclass MigrationEngine:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: IdMappingService | None = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n # #endregion __init__\n # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION DEPENDS_ON -> [MigrationEngine]\n # @PARAM zip_path (str) - Path to the source ZIP file.\n # @PARAM output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM file_path (Path) - Path to the YAML file.\n # @PARAM db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT Input[Path] -> Output[Dict[int,str]]\n # @PARAM temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n logger.debug(\"Could not parse chart YAML file %s\", cf)\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM file_path (Path)\n # @PARAM target_env_id (str)\n # @PARAM source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n# #endregion MigrationEngine\n# #endregion MigrationEngineModule\n" }, { "contract_id": "MigrationEngine", @@ -22464,14 +22556,14 @@ { "source_id": "MigrationEngine", "relation_type": "DEPENDS_ON", - "target_id": "[EXT:list:MigrationEngine_internal_methods]", - "target_ref": "[[EXT:list:MigrationEngine_internal_methods]]" + "target_id": "MigrationEngine", + "target_ref": "[MigrationEngine]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MigrationEngine [TYPE Class]\n# @BRIEF Engine for transforming Superset export ZIPs.\n# @RELATION DEPENDS_ON -> [[EXT:list:MigrationEngine_internal_methods]]\nclass MigrationEngine:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: IdMappingService | None = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n # #endregion __init__\n # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION DEPENDS_ON -> [[EXT:list:MigrateEngine_transform_methods]]\n # @PARAM zip_path (str) - Path to the source ZIP file.\n # @PARAM output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM file_path (Path) - Path to the YAML file.\n # @PARAM db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT Input[Path] -> Output[Dict[int,str]]\n # @PARAM temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n logger.debug(\"Could not parse chart YAML file %s\", cf)\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM file_path (Path)\n # @PARAM target_env_id (str)\n # @PARAM source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n# #endregion MigrationEngine\n" + "body": "# #region MigrationEngine [TYPE Class]\n# @BRIEF Engine for transforming Superset export ZIPs.\n# @RELATION DEPENDS_ON -> [MigrationEngine]\nclass MigrationEngine:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: IdMappingService | None = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n # #endregion __init__\n # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION DEPENDS_ON -> [MigrationEngine]\n # @PARAM zip_path (str) - Path to the source ZIP file.\n # @PARAM output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM file_path (Path) - Path to the YAML file.\n # @PARAM db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT Input[Path] -> Output[Dict[int,str]]\n # @PARAM temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n logger.debug(\"Could not parse chart YAML file %s\", cf)\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM file_path (Path)\n # @PARAM target_env_id (str)\n # @PARAM source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n# #endregion MigrationEngine\n" }, { "contract_id": "transform_zip", @@ -22494,14 +22586,14 @@ { "source_id": "transform_zip", "relation_type": "DEPENDS_ON", - "target_id": "[EXT:list:MigrateEngine_transform_methods]", - "target_ref": "[[EXT:list:MigrateEngine_transform_methods]]" + "target_id": "MigrationEngine", + "target_ref": "[MigrationEngine]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION DEPENDS_ON -> [[EXT:list:MigrateEngine_transform_methods]]\n # @PARAM zip_path (str) - Path to the source ZIP file.\n # @PARAM output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n" + "body": " # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION DEPENDS_ON -> [MigrationEngine]\n # @PARAM zip_path (str) - Path to the source ZIP file.\n # @PARAM output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n" }, { "contract_id": "_transform_yaml", @@ -24343,7 +24435,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SupersetDashboardsWriteMixin [C:4] [TYPE Module] [SEMANTICS superset, dashboard, chart, write, maintenance, banner]\n# @BRIEF Dashboard write mixin for SupersetClient — markdown chart CRUD and layout manipulation for maintenance banners.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetChartsMixin]\n# @RELATION DEPENDS_ON -> [LayoutUtils]\n# @SIDE_EFFECT Creates, updates, deletes markdown charts & modifies dashboard layout via Superset API.\n# @DATA_CONTRACT Input: dashboard_id / chart_id / markdown_text → Output: API response dict or chart_id int\n# @INVARIANT All network operations use self.network.request()\n# @RATIONALE Extracted from monolithic superset_client to satisfy INV_7.\n# Uses belief_scope with REASON/REFLECT/EXPLORE markers for all C4 operations per Std.Semantics.Python.\n\nimport json\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope, logger as app_logger\nfrom ._layout[EXT:internal:_utils] import (\n insert_banner_markdown_at_top,\n parse_position_json,\n remove_banner_from_position,\n update_banner_markdown_content,\n)\n\napp_logger = cast(Any, app_logger)\n\n\n# #region SupersetDashboardsWriteMixin [C:4] [TYPE Class]\n# @BRIEF Mixin providing markdown chart CRUD and dashboard layout manipulation for maintenance banners.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetChartsMixin]\nclass SupersetDashboardsWriteMixin:\n \"\"\"Mixin for creating/updating/deleting markdown charts and manipulating dashboard layouts.\"\"\"\n\n # #region create_markdown_chart [C:3] [TYPE Function]\n # @BRIEF Create a markdown chart and return the new chart ID.\n # @PRE dashboard_id exists in Superset. markdown_text not empty.\n # @POST Returns chart_id (int). Chart is NOT yet placed in dashboard layout.\n # @SIDE_EFFECT Creates a new chart resource in Superset.\n # @RELATION CALLS -> [EXT:method:self.network.request]\n def create_markdown_chart(self, dashboard_id: int, markdown_text: str) -> int:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.create_markdown_chart\",\n f\"dashboard_id={dashboard_id}\",\n ):\n app_logger.reason(\n \"Creating markdown chart\",\n extra={\"dashboard_id\": dashboard_id, \"len\": len(markdown_text)},\n )\n datasource_id = self._resolve_markdown_datasource(dashboard_id)\n payload = {\n \"dashboards\": [dashboard_id],\n \"datasource_id\": datasource_id,\n \"datasource_type\": \"table\",\n \"slice_name\": \"Maintenance Banner\",\n \"viz_type\": \"markdown\",\n \"params\": json.dumps({\n \"markdown\": markdown_text,\n \"viz_type\": \"markdown\",\n }),\n }\n try:\n response = cast(dict, self.network.request(\n method=\"POST\", endpoint=\"/chart/\", json=payload\n ))\n chart_id = int(response.get(\"id\") or response.get(\"result\", {}).get(\"id\", 0))\n app_logger.reflect(\"Chart created\", extra={\"chart_id\": chart_id})\n return chart_id\n except Exception as e:\n app_logger.explore(\"Create chart failed\", extra={\"dashboard_id\": dashboard_id}, error=str(e))\n raise\n # #endregion create_markdown_chart\n\n # #region update_markdown_chart [C:3] [TYPE Function]\n # @BRIEF Update the markdown text of an existing markdown chart.\n # @PRE chart_id exists and is a markdown chart.\n # @POST Chart markdown content updated.\n # @SIDE_EFFECT Modifies a chart resource in Superset.\n def update_markdown_chart(self, chart_id: int, markdown_text: str) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.update_markdown_chart\",\n f\"chart_id={chart_id}\",\n ):\n app_logger.reason(\"Updating markdown chart\", extra={\"chart_id\": chart_id, \"len\": len(markdown_text)})\n payload = {\"params\": json.dumps({\"markdown\": markdown_text, \"viz_type\": \"markdown\"})}\n try:\n response = cast(dict, self.network.request(method=\"PUT\", endpoint=f\"/chart/{chart_id}\", json=payload))\n app_logger.reflect(\"Chart updated\", extra={\"chart_id\": chart_id})\n return response\n except Exception as e:\n app_logger.explore(\"Update chart failed\", extra={\"chart_id\": chart_id}, error=str(e))\n raise\n # #endregion update_markdown_chart\n\n # #region update_dashboard_layout [C:4] [TYPE Function]\n # @BRIEF Insert a native MARKDOWN element at (0,0) with full width (12 cols),\n # shift existing charts down. Uses native MARKDOWN for reliable rendering.\n # @PRE dashboard_id exists in Superset. chart_id is used for tracking/DB.\n # @POST MARKDOWN element placed at top; existing items shifted down.\n # @SIDE_EFFECT Modifies dashboard layout JSON.\n def update_dashboard_layout(self, dashboard_id: int, chart_id: int) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.update_dashboard_layout\",\n f\"dashboard_id={dashboard_id}, chart_id={chart_id}\",\n ):\n app_logger.reason(\n \"Updating layout with native MARKDOWN\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n try:\n dashboard = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n ))\n result = dashboard.get(\"result\", dashboard)\n position_json = parse_position_json(result.get(\"position_json\", \"{}\"))\n\n # Use native MARKDOWN element inside a ROW, connected to GRID_ID\n placeholder = \"*Maintenance banner*\"\n insert_banner_markdown_at_top(position_json, chart_id, placeholder)\n\n response = cast(dict, self.network.request(\n method=\"PUT\", endpoint=f\"/dashboard/{dashboard_id}\",\n json={\"position_json\": json.dumps(position_json)},\n ))\n app_logger.reflect(\n \"Layout updated with native MARKDOWN\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n return response\n except Exception as e:\n app_logger.explore(\n \"Layout update failed\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n # #endregion update_dashboard_layout\n\n # #region remove_chart_from_layout [C:3] [TYPE Function]\n # @BRIEF Remove banner markdown element from dashboard layout without deleting the chart.\n # @PRE dashboard_id exists. chart_id exists in layout as MARKDOWN-banner-{chart_id}.\n # @POST MARKDOWN element removed from position_json; items shifted up.\n # @SIDE_EFFECT Modifies dashboard layout JSON.\n def remove_chart_from_layout(self, dashboard_id: int, chart_id: int) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.remove_chart_from_layout\",\n f\"dashboard_id={dashboard_id}, chart_id={chart_id}\",\n ):\n app_logger.reason(\n \"Removing banner from layout\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n try:\n dashboard = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n ))\n result = dashboard.get(\"result\", dashboard)\n position_json = parse_position_json(result.get(\"position_json\", \"{}\"))\n # Remove new MARKDOWN+ROW pair (old CHART-{id} format no longer created)\n remove_banner_from_position(position_json, chart_id)\n\n response = cast(dict, self.network.request(\n method=\"PUT\", endpoint=f\"/dashboard/{dashboard_id}\",\n json={\"position_json\": json.dumps(position_json)},\n ))\n app_logger.reflect(\n \"Banner removed from layout\",\n extra={\"dashboard_id\": dashboard_id},\n )\n return response\n except Exception as e:\n app_logger.explore(\n \"Remove from layout failed\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n # #endregion remove_chart_from_layout\n\n # #region delete_chart [C:3] [TYPE Function]\n # @BRIEF Delete a chart from Superset by ID.\n # @PRE chart_id exists.\n # @POST Chart permanently deleted from Superset.\n # @SIDE_EFFECT Destroys chart resource.\n def delete_chart(self, chart_id: int) -> dict:\n with belief_scope(\"SupersetDashboardsWriteMixin.delete_chart\", f\"chart_id={chart_id}\"):\n app_logger.reason(\"Deleting chart\", extra={\"chart_id\": chart_id})\n try:\n response = cast(dict, self.network.request(method=\"DELETE\", endpoint=f\"/chart/{chart_id}\"))\n app_logger.reflect(\"Chart deleted\", extra={\"chart_id\": chart_id})\n return response\n except Exception as e:\n app_logger.explore(\"Delete chart failed\", extra={\"chart_id\": chart_id}, error=str(e))\n raise\n # #endregion delete_chart\n\n # #region update_banner_on_dashboard [C:3] [TYPE Function]\n # @BRIEF Update the content of a native MARKDOWN banner element on a dashboard.\n # @PRE dashboard_id exists. chart_id used for key lookup.\n # @POST MARKDOWN element content updated on dashboard.\n # @SIDE_EFFECT Modifies dashboard layout JSON in Superset.\n def update_banner_on_dashboard(\n self, dashboard_id: int, chart_id: int, content: str\n ) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.update_banner_on_dashboard\",\n f\"dashboard_id={dashboard_id}, chart_id={chart_id}\",\n ):\n app_logger.reason(\n \"Updating banner content on dashboard\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id, \"len\": len(content)},\n )\n try:\n dashboard = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n ))\n result = dashboard.get(\"result\", dashboard)\n position_json = parse_position_json(result.get(\"position_json\", \"{}\"))\n markdown_key = f\"MARKDOWN-banner-{chart_id}\"\n update_banner_markdown_content(position_json, markdown_key, content)\n response = cast(dict, self.network.request(\n method=\"PUT\", endpoint=f\"/dashboard/{dashboard_id}\",\n json={\"position_json\": json.dumps(position_json)},\n ))\n app_logger.reflect(\n \"Banner content updated on dashboard\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n return response\n except Exception as e:\n app_logger.explore(\n \"Update banner content failed\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n # #endregion update_banner_on_dashboard\n\n\n # #region get_dashboard_layout [C:3] [TYPE Function]\n # @BRIEF Fetch the position_json layout of a dashboard.\n # @PRE dashboard_id exists.\n # @POST Returns the dashboard layout dict.\n def get_dashboard_layout(self, dashboard_id: int) -> dict:\n with belief_scope(\"SupersetDashboardsWriteMixin.get_dashboard_layout\", f\"dashboard_id={dashboard_id}\"):\n app_logger.reason(\"Fetching layout\", extra={\"dashboard_id\": dashboard_id})\n try:\n dashboard = cast(dict, self.network.request(method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"))\n result = dashboard.get(\"result\", dashboard)\n layout = parse_position_json(result.get(\"position_json\", \"{}\"))\n app_logger.reflect(\"Layout fetched\", extra={\"dashboard_id\": dashboard_id, \"items\": len(layout)})\n return layout\n except Exception as e:\n app_logger.explore(\"Fetch layout failed\", extra={\"dashboard_id\": dashboard_id}, error=str(e))\n raise\n # #endregion get_dashboard_layout\n\n # #region _resolve_markdown_datasource [C:2] [TYPE Function]\n # @BRIEF Find a valid datasource_id for markdown chart creation.\n # @PRE dashboard_id exists.\n # @POST Returns an integer datasource_id.\n def _resolve_markdown_datasource(self, dashboard_id: int) -> int:\n with belief_scope(\"_resolve_markdown_datasource\", f\"dashboard_id={dashboard_id}\"):\n try:\n datasets_response = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}/datasets\"\n ))\n datasets = datasets_response.get(\"result\", [])\n if datasets:\n ds_id = datasets[0].get(\"id\")\n if ds_id:\n return int(ds_id)\n except Exception:\n app_logger.explore(\"Dashboard datasets fetch failed, falling back\",\n extra={\"dashboard_id\": dashboard_id}, error=\"Dashboard datasets endpoint failed\")\n try:\n all_ds = cast(dict, self.network.request(\n method=\"GET\", endpoint=\"/dataset/\",\n params={\"q\": json.dumps({\"columns\": [\"id\"], \"page_size\": 1, \"page\": 0})},\n ))\n ds_list = all_ds.get(\"result\", [])\n if ds_list:\n ds_id = ds_list[0].get(\"id\")\n if ds_id:\n return int(ds_id)\n except Exception:\n app_logger.explore(\"No datasets available\", extra={}, error=\"Global dataset list failed\")\n raise RuntimeError(\"Cannot create markdown chart: no datasets available in Superset.\")\n # #endregion _resolve_markdown_datasource\n\n# #endregion SupersetDashboardsWriteMixin\n# #endregion SupersetDashboardsWriteMixin\n" + "body": "# #region SupersetDashboardsWriteMixin [C:4] [TYPE Module] [SEMANTICS superset, dashboard, chart, write, maintenance, banner]\n# @BRIEF Dashboard write mixin for SupersetClient — markdown chart CRUD and layout manipulation for maintenance banners.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetChartsMixin]\n# @RELATION DEPENDS_ON -> [LayoutUtils]\n# @SIDE_EFFECT Creates, updates, deletes markdown charts & modifies dashboard layout via Superset API.\n# @DATA_CONTRACT Input: dashboard_id / chart_id / markdown_text → Output: API response dict or chart_id int\n# @INVARIANT All network operations use self.network.request()\n# @RATIONALE Extracted from monolithic superset_client to satisfy INV_7.\n# Uses belief_scope with REASON/REFLECT/EXPLORE markers for all C4 operations per Std.Semantics.Python.\n\nimport json\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope, logger as app_logger\nfrom ._layout_utils import (\n insert_banner_markdown_at_top,\n parse_position_json,\n remove_banner_from_position,\n update_banner_markdown_content,\n)\n\napp_logger = cast(Any, app_logger)\n\n\n# #region SupersetDashboardsWriteMixin [C:4] [TYPE Class]\n# @BRIEF Mixin providing markdown chart CRUD and dashboard layout manipulation for maintenance banners.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetChartsMixin]\nclass SupersetDashboardsWriteMixin:\n \"\"\"Mixin for creating/updating/deleting markdown charts and manipulating dashboard layouts.\"\"\"\n\n # #region create_markdown_chart [C:3] [TYPE Function]\n # @BRIEF Create a markdown chart and return the new chart ID.\n # @PRE dashboard_id exists in Superset. markdown_text not empty.\n # @POST Returns chart_id (int). Chart is NOT yet placed in dashboard layout.\n # @SIDE_EFFECT Creates a new chart resource in Superset.\n # @RELATION CALLS -> [EXT:method:self.network.request]\n def create_markdown_chart(self, dashboard_id: int, markdown_text: str) -> int:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.create_markdown_chart\",\n f\"dashboard_id={dashboard_id}\",\n ):\n app_logger.reason(\n \"Creating markdown chart\",\n extra={\"dashboard_id\": dashboard_id, \"len\": len(markdown_text)},\n )\n datasource_id = self._resolve_markdown_datasource(dashboard_id)\n payload = {\n \"dashboards\": [dashboard_id],\n \"datasource_id\": datasource_id,\n \"datasource_type\": \"table\",\n \"slice_name\": \"Maintenance Banner\",\n \"viz_type\": \"markdown\",\n \"params\": json.dumps({\n \"markdown\": markdown_text,\n \"viz_type\": \"markdown\",\n }),\n }\n try:\n response = cast(dict, self.network.request(\n method=\"POST\", endpoint=\"/chart/\", json=payload\n ))\n chart_id = int(response.get(\"id\") or response.get(\"result\", {}).get(\"id\", 0))\n app_logger.reflect(\"Chart created\", extra={\"chart_id\": chart_id})\n return chart_id\n except Exception as e:\n app_logger.explore(\"Create chart failed\", extra={\"dashboard_id\": dashboard_id}, error=str(e))\n raise\n # #endregion create_markdown_chart\n\n # #region update_markdown_chart [C:3] [TYPE Function]\n # @BRIEF Update the markdown text of an existing markdown chart.\n # @PRE chart_id exists and is a markdown chart.\n # @POST Chart markdown content updated.\n # @SIDE_EFFECT Modifies a chart resource in Superset.\n def update_markdown_chart(self, chart_id: int, markdown_text: str) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.update_markdown_chart\",\n f\"chart_id={chart_id}\",\n ):\n app_logger.reason(\"Updating markdown chart\", extra={\"chart_id\": chart_id, \"len\": len(markdown_text)})\n payload = {\"params\": json.dumps({\"markdown\": markdown_text, \"viz_type\": \"markdown\"})}\n try:\n response = cast(dict, self.network.request(method=\"PUT\", endpoint=f\"/chart/{chart_id}\", json=payload))\n app_logger.reflect(\"Chart updated\", extra={\"chart_id\": chart_id})\n return response\n except Exception as e:\n app_logger.explore(\"Update chart failed\", extra={\"chart_id\": chart_id}, error=str(e))\n raise\n # #endregion update_markdown_chart\n\n # #region update_dashboard_layout [C:4] [TYPE Function]\n # @BRIEF Insert a native MARKDOWN element at (0,0) with full width (12 cols),\n # shift existing charts down. Uses native MARKDOWN for reliable rendering.\n # @PRE dashboard_id exists in Superset. chart_id is used for tracking/DB.\n # @POST MARKDOWN element placed at top; existing items shifted down.\n # @SIDE_EFFECT Modifies dashboard layout JSON.\n def update_dashboard_layout(self, dashboard_id: int, chart_id: int) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.update_dashboard_layout\",\n f\"dashboard_id={dashboard_id}, chart_id={chart_id}\",\n ):\n app_logger.reason(\n \"Updating layout with native MARKDOWN\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n try:\n dashboard = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n ))\n result = dashboard.get(\"result\", dashboard)\n position_json = parse_position_json(result.get(\"position_json\", \"{}\"))\n\n # Use native MARKDOWN element inside a ROW, connected to GRID_ID\n placeholder = \"*Maintenance banner*\"\n insert_banner_markdown_at_top(position_json, chart_id, placeholder)\n\n response = cast(dict, self.network.request(\n method=\"PUT\", endpoint=f\"/dashboard/{dashboard_id}\",\n json={\"position_json\": json.dumps(position_json)},\n ))\n app_logger.reflect(\n \"Layout updated with native MARKDOWN\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n return response\n except Exception as e:\n app_logger.explore(\n \"Layout update failed\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n # #endregion update_dashboard_layout\n\n # #region remove_chart_from_layout [C:3] [TYPE Function]\n # @BRIEF Remove banner markdown element from dashboard layout without deleting the chart.\n # @PRE dashboard_id exists. chart_id exists in layout as MARKDOWN-banner-{chart_id}.\n # @POST MARKDOWN element removed from position_json; items shifted up.\n # @SIDE_EFFECT Modifies dashboard layout JSON.\n def remove_chart_from_layout(self, dashboard_id: int, chart_id: int) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.remove_chart_from_layout\",\n f\"dashboard_id={dashboard_id}, chart_id={chart_id}\",\n ):\n app_logger.reason(\n \"Removing banner from layout\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n try:\n dashboard = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n ))\n result = dashboard.get(\"result\", dashboard)\n position_json = parse_position_json(result.get(\"position_json\", \"{}\"))\n # Remove new MARKDOWN+ROW pair (old CHART-{id} format no longer created)\n remove_banner_from_position(position_json, chart_id)\n\n response = cast(dict, self.network.request(\n method=\"PUT\", endpoint=f\"/dashboard/{dashboard_id}\",\n json={\"position_json\": json.dumps(position_json)},\n ))\n app_logger.reflect(\n \"Banner removed from layout\",\n extra={\"dashboard_id\": dashboard_id},\n )\n return response\n except Exception as e:\n app_logger.explore(\n \"Remove from layout failed\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n # #endregion remove_chart_from_layout\n\n # #region delete_chart [C:3] [TYPE Function]\n # @BRIEF Delete a chart from Superset by ID.\n # @PRE chart_id exists.\n # @POST Chart permanently deleted from Superset.\n # @SIDE_EFFECT Destroys chart resource.\n def delete_chart(self, chart_id: int) -> dict:\n with belief_scope(\"SupersetDashboardsWriteMixin.delete_chart\", f\"chart_id={chart_id}\"):\n app_logger.reason(\"Deleting chart\", extra={\"chart_id\": chart_id})\n try:\n response = cast(dict, self.network.request(method=\"DELETE\", endpoint=f\"/chart/{chart_id}\"))\n app_logger.reflect(\"Chart deleted\", extra={\"chart_id\": chart_id})\n return response\n except Exception as e:\n app_logger.explore(\"Delete chart failed\", extra={\"chart_id\": chart_id}, error=str(e))\n raise\n # #endregion delete_chart\n\n # #region update_banner_on_dashboard [C:3] [TYPE Function]\n # @BRIEF Update the content of a native MARKDOWN banner element on a dashboard.\n # @PRE dashboard_id exists. chart_id used for key lookup.\n # @POST MARKDOWN element content updated on dashboard.\n # @SIDE_EFFECT Modifies dashboard layout JSON in Superset.\n def update_banner_on_dashboard(\n self, dashboard_id: int, chart_id: int, content: str\n ) -> dict:\n with belief_scope(\n \"SupersetDashboardsWriteMixin.update_banner_on_dashboard\",\n f\"dashboard_id={dashboard_id}, chart_id={chart_id}\",\n ):\n app_logger.reason(\n \"Updating banner content on dashboard\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id, \"len\": len(content)},\n )\n try:\n dashboard = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n ))\n result = dashboard.get(\"result\", dashboard)\n position_json = parse_position_json(result.get(\"position_json\", \"{}\"))\n markdown_key = f\"MARKDOWN-banner-{chart_id}\"\n update_banner_markdown_content(position_json, markdown_key, content)\n response = cast(dict, self.network.request(\n method=\"PUT\", endpoint=f\"/dashboard/{dashboard_id}\",\n json={\"position_json\": json.dumps(position_json)},\n ))\n app_logger.reflect(\n \"Banner content updated on dashboard\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n )\n return response\n except Exception as e:\n app_logger.explore(\n \"Update banner content failed\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n # #endregion update_banner_on_dashboard\n\n\n # #region get_dashboard_layout [C:3] [TYPE Function]\n # @BRIEF Fetch the position_json layout of a dashboard.\n # @PRE dashboard_id exists.\n # @POST Returns the dashboard layout dict.\n def get_dashboard_layout(self, dashboard_id: int) -> dict:\n with belief_scope(\"SupersetDashboardsWriteMixin.get_dashboard_layout\", f\"dashboard_id={dashboard_id}\"):\n app_logger.reason(\"Fetching layout\", extra={\"dashboard_id\": dashboard_id})\n try:\n dashboard = cast(dict, self.network.request(method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"))\n result = dashboard.get(\"result\", dashboard)\n layout = parse_position_json(result.get(\"position_json\", \"{}\"))\n app_logger.reflect(\"Layout fetched\", extra={\"dashboard_id\": dashboard_id, \"items\": len(layout)})\n return layout\n except Exception as e:\n app_logger.explore(\"Fetch layout failed\", extra={\"dashboard_id\": dashboard_id}, error=str(e))\n raise\n # #endregion get_dashboard_layout\n\n # #region _resolve_markdown_datasource [C:2] [TYPE Function]\n # @BRIEF Find a valid datasource_id for markdown chart creation.\n # @PRE dashboard_id exists.\n # @POST Returns an integer datasource_id.\n def _resolve_markdown_datasource(self, dashboard_id: int) -> int:\n with belief_scope(\"_resolve_markdown_datasource\", f\"dashboard_id={dashboard_id}\"):\n try:\n datasets_response = cast(dict, self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}/datasets\"\n ))\n datasets = datasets_response.get(\"result\", [])\n if datasets:\n ds_id = datasets[0].get(\"id\")\n if ds_id:\n return int(ds_id)\n except Exception:\n app_logger.explore(\"Dashboard datasets fetch failed, falling back\",\n extra={\"dashboard_id\": dashboard_id}, error=\"Dashboard datasets endpoint failed\")\n try:\n all_ds = cast(dict, self.network.request(\n method=\"GET\", endpoint=\"/dataset/\",\n params={\"q\": json.dumps({\"columns\": [\"id\"], \"page_size\": 1, \"page\": 0})},\n ))\n ds_list = all_ds.get(\"result\", [])\n if ds_list:\n ds_id = ds_list[0].get(\"id\")\n if ds_id:\n return int(ds_id)\n except Exception:\n app_logger.explore(\"No datasets available\", extra={}, error=\"Global dataset list failed\")\n raise RuntimeError(\"Cannot create markdown chart: no datasets available in Superset.\")\n # #endregion _resolve_markdown_datasource\n\n# #endregion SupersetDashboardsWriteMixin\n# #endregion SupersetDashboardsWriteMixin\n" }, { "contract_id": "create_markdown_chart", @@ -25461,14 +25553,14 @@ { "source_id": "__tests__/test_task_logger", "relation_type": "BINDS_TO", - "target_id": "../task_logger.py", - "target_ref": "../task_logger.py" + "target_id": "EXT:path:task_logger", + "target_ref": "[EXT:path:task_logger]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region __tests__/test_task_logger [TYPE Module] [SEMANTICS test, task, logger, contract]\n# @RELATION BINDS_TO -> ../task_logger.py\n# @BRIEF Contract testing for TaskLogger\n# #endregion __tests__/test_task_logger\n" + "body": "# #region __tests__/test_task_logger [TYPE Module] [SEMANTICS test, task, logger, contract]\n# @RELATION BINDS_TO -> [EXT:path:task_logger]\n# @BRIEF Contract testing for TaskLogger\n# #endregion __tests__/test_task_logger\n" }, { "contract_id": "test_task_logger_initialization", @@ -33707,13 +33799,13 @@ "source_id": "LlmModels", "relation_type": "INHERITS", "target_id": "MappingModels", - "target_ref": "MappingModels:Base" + "target_ref": "[MappingModels]" } ], "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:Base\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 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" }, { "contract_id": "ValidationPolicy", @@ -33797,14 +33889,14 @@ { "source_id": "MaintenanceModels", "relation_type": "DEPENDS_ON", - "target_id": "EXT:internal:MappingBase", - "target_ref": "[EXT:internal:MappingBase]" + "target_id": "MappingModels", + "target_ref": "[MappingModels]" } ], "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 -> [EXT:internal:MappingBase]\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 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" }, { "contract_id": "MaintenanceEventStatus", @@ -33973,7 +34065,7 @@ "contract_type": "Module", "file_path": "backend/src/models/mapping.py", "start_line": 1, - "end_line": 99, + "end_line": 102, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -33993,14 +34085,32 @@ "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\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.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" + }, + { + "contract_id": "Base", + "contract_type": "Class", + "file_path": "backend/src/models/mapping.py", + "start_line": 22, + "end_line": 24, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "SQLAlchemy declarative base for all domain models.", + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region Base [C:1] [TYPE Class]\n# @BRIEF SQLAlchemy declarative base for all domain models.\n# #endregion Base\n" }, { "contract_id": "ResourceType", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 24, - "end_line": 30, + "start_line": 27, + "end_line": 33, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -34017,8 +34127,8 @@ "contract_id": "MigrationStatus", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 33, - "end_line": 41, + "start_line": 36, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -34035,8 +34145,8 @@ "contract_id": "MigrationJob", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 70, - "end_line": 81, + "start_line": 73, + "end_line": 84, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -34053,8 +34163,8 @@ "contract_id": "ResourceMapping", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 83, - "end_line": 97, + "start_line": 86, + "end_line": 100, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -34103,14 +34213,14 @@ { "source_id": "ProfileModels", "relation_type": "INHERITS", - "target_id": "EXT:internal:MappingModels:Base", - "target_ref": "[EXT:internal:MappingModels:Base]" + "target_id": "MappingModels", + "target_ref": "[MappingModels]" } ], "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 -> [EXT:internal:MappingModels:Base]\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:Base\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 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" }, { "contract_id": "UserDashboardPreference", @@ -34129,13 +34239,13 @@ "source_id": "UserDashboardPreference", "relation_type": "INHERITS", "target_id": "MappingModels", - "target_ref": "MappingModels:Base" + "target_ref": "[MappingModels]" } ], "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:Base\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=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" }, { "contract_id": "ReportModels", @@ -34929,8 +35039,8 @@ { "source_id": "GitPluginModule", "relation_type": "INHERITS", - "target_id": "EXT:path:src.core.plugin_base.PluginBase", - "target_ref": "[EXT:path:src.core.plugin_base.PluginBase]" + "target_id": "PluginBase", + "target_ref": "[PluginBase]" }, { "source_id": "GitPluginModule", @@ -34960,7 +35070,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 -> [EXT:path:src.core.plugin_base.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)\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" }, { "contract_id": "GitPlugin", @@ -35155,7 +35265,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 -> [EXT:path:backend.src.core.plugin_base.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 -> [EXT:path:backend.src.core.plugin_base.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 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" }, { "contract_id": "_is_masked_or_invalid_api_key", @@ -35210,14 +35320,14 @@ { "source_id": "DashboardValidationPlugin", "relation_type": "IMPLEMENTS", - "target_id": "EXT:path:backend.src.core.plugin_base.PluginBase", - "target_ref": "[EXT:path:backend.src.core.plugin_base.PluginBase]" + "target_id": "PluginBase", + "target_ref": "[PluginBase]" } ], "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 -> [EXT:path:backend.src.core.plugin_base.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.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" }, { "contract_id": "DocumentationPlugin", @@ -35234,14 +35344,14 @@ { "source_id": "DocumentationPlugin", "relation_type": "IMPLEMENTS", - "target_id": "EXT:path:backend.src.core.plugin_base.PluginBase", - "target_ref": "[EXT:path:backend.src.core.plugin_base.PluginBase]" + "target_id": "PluginBase", + "target_ref": "[PluginBase]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DocumentationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dataset documentation using LLMs.\n# @RELATION IMPLEMENTS -> [EXT:path:backend.src.core.plugin_base.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" + "body": "# #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" }, { "contract_id": "LLMAnalysisScheduler", @@ -35706,7 +35816,7 @@ "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[EXT:internal:_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[EXT:internal:_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: 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" }, { "contract_id": "TestDictionaryCorrection", @@ -35858,14 +35968,14 @@ { "source_id": "TestDictionaryUtils", "relation_type": "BINDS_TO", - "target_id": "[EXT:internal:_utils]", - "target_ref": "[[EXT:internal:_utils]]" + "target_id": "TranslationUtils", + "target_ref": "[TranslationUtils]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDictionaryUtils [C:3] [TYPE Module] [SEMANTICS test, dictionary, utils, normalization]\n# @BRIEF Validate utility functions: _normalize_term and _detect_delimiter.\n# @RELATION BINDS_TO -> [[EXT:internal:_utils]]\n\nfrom src.plugins.translate.[EXT:internal:_utils] import _detect_delimiter, _normalize_term\n\n\nclass TestNormalizeTerm:\n \"\"\"Verify _normalize_term produces lowercase NFC-normalized strings.\"\"\"\n\n # region test_normalize_term [C:2] [TYPE Function]\n def test_normalize_term(self):\n assert _normalize_term(\"Hello\") == \"hello\"\n assert _normalize_term(\"HELLO\") == \"hello\"\n assert _normalize_term(\" hello \") == \"hello\"\n composed = \"\\u00C9\"\n decomposed = \"\\u0045\\u0301\"\n assert _normalize_term(composed) == _normalize_term(decomposed)\n # endregion test_normalize_term\n\n\nclass TestDetectDelimiter:\n \"\"\"Verify _detect_delimiter correctly identifies CSV vs TSV.\"\"\"\n\n # region test_detect_delimiter [C:2] [TYPE Function]\n def test_detect_delimiter(self):\n csv_content = \"source_term,target_term,context_notes\\nhello,hola,\"\n assert _detect_delimiter(csv_content) == \",\"\n\n tsv_content = \"source_term\\ttarget_term\\nhello\\thola\"\n assert _detect_delimiter(tsv_content) == \"\\t\"\n\n empty_content = \"\"\n assert _detect_delimiter(empty_content) == \",\"\n # endregion test_detect_delimiter\n# #endregion TestDictionaryUtils\n" + "body": "# #region TestDictionaryUtils [C:3] [TYPE Module] [SEMANTICS test, dictionary, utils, normalization]\n# @BRIEF Validate utility functions: _normalize_term and _detect_delimiter.\n# @RELATION BINDS_TO -> [TranslationUtils]\n\nfrom src.plugins.translate._utils import _detect_delimiter, _normalize_term\n\n\nclass TestNormalizeTerm:\n \"\"\"Verify _normalize_term produces lowercase NFC-normalized strings.\"\"\"\n\n # region test_normalize_term [C:2] [TYPE Function]\n def test_normalize_term(self):\n assert _normalize_term(\"Hello\") == \"hello\"\n assert _normalize_term(\"HELLO\") == \"hello\"\n assert _normalize_term(\" hello \") == \"hello\"\n composed = \"\\u00C9\"\n decomposed = \"\\u0045\\u0301\"\n assert _normalize_term(composed) == _normalize_term(decomposed)\n # endregion test_normalize_term\n\n\nclass TestDetectDelimiter:\n \"\"\"Verify _detect_delimiter correctly identifies CSV vs TSV.\"\"\"\n\n # region test_detect_delimiter [C:2] [TYPE Function]\n def test_detect_delimiter(self):\n csv_content = \"source_term,target_term,context_notes\\nhello,hola,\"\n assert _detect_delimiter(csv_content) == \",\"\n\n tsv_content = \"source_term\\ttarget_term\\nhello\\thola\"\n assert _detect_delimiter(tsv_content) == \"\\t\"\n\n empty_content = \"\"\n assert _detect_delimiter(empty_content) == \",\"\n # endregion test_detect_delimiter\n# #endregion TestDictionaryUtils\n" }, { "contract_id": "LanguageDetectServiceTests", @@ -36029,14 +36139,14 @@ { "source_id": "TestTextCleaner", "relation_type": "BINDS_TO", - "target_id": "[EXT:internal:_text_cleaner]", - "target_ref": "[[EXT:internal:_text_cleaner]]" + "target_id": "TextCleaner", + "target_ref": "[TextCleaner]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestTextCleaner [C:3] [TYPE Module] [SEMANTICS test, text, cleaner, whitespace, truncation]\n# @BRIEF Verify text cleaning contracts — normalize_whitespace, truncate_text, clean_text.\n# @RELATION BINDS_TO -> [[EXT:internal:_text_cleaner]]\n# @TEST_EDGE empty_string — empty/whitespace-only input returns \"\"\n# @TEST_EDGE whitespace_variants — multiple spaces, newlines, tabs all collapse to single space\n# @TEST_EDGE truncation_boundary — text shorter than, equal to, and longer than max_length\n# @TEST_EDGE clean_combined — clean_text correctly chains normalize + truncate\n# @TEST_EDGE zero_max_length — max_length=0 forces truncation on any non-empty text\n\nfrom src.plugins.translate.[EXT:internal:_text_cleaner] import clean_text, normalize_whitespace, truncate_text\n\n\n# region TestNormalizeWhitespace [TYPE Class]\n# @BRIEF Test suite for normalize_whitespace.\nclass TestNormalizeWhitespace:\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string returns empty string.\n def test_empty_string(self):\n assert normalize_whitespace(\"\") == \"\"\n # endregion test_empty_string\n\n # region test_whitespace_only [TYPE Function]\n # @BRIEF String with only whitespace returns empty string.\n def test_whitespace_only(self):\n assert normalize_whitespace(\" \") == \"\"\n assert normalize_whitespace(\"\\t\") == \"\"\n assert normalize_whitespace(\"\\n\") == \"\"\n # endregion test_whitespace_only\n\n # region test_multiple_spaces [TYPE Function]\n # @BRIEF Multiple spaces between words are collapsed to single space.\n def test_multiple_spaces(self):\n assert normalize_whitespace(\"hello world\") == \"hello world\"\n # endregion test_multiple_spaces\n\n # region test_multiple_newlines [TYPE Function]\n # @BRIEF Multiple newlines are collapsed to single space.\n def test_multiple_newlines(self):\n assert normalize_whitespace(\"hello\\n\\n\\nworld\") == \"hello world\"\n # endregion test_multiple_newlines\n\n # region test_mixed_whitespace [TYPE Function]\n # @BRIEF Tabs, newlines, and spaces all collapse to single space.\n def test_mixed_whitespace(self):\n assert normalize_whitespace(\"hello\\t \\n world\") == \"hello world\"\n # endregion test_mixed_whitespace\n\n # region test_leading_trailing_whitespace [TYPE Function]\n # @BRIEF Leading and trailing whitespace is trimmed.\n def test_leading_trailing_whitespace(self):\n assert normalize_whitespace(\" hello world \") == \"hello world\"\n assert normalize_whitespace(\"\\nhello world\\n\") == \"hello world\"\n # endregion test_leading_trailing_whitespace\n\n # region test_already_clean [TYPE Function]\n # @BRIEF Already clean text is returned unchanged.\n def test_already_clean(self):\n assert normalize_whitespace(\"hello world\") == \"hello world\"\n # endregion test_already_clean\n\n # region test_single_word [TYPE Function]\n # @BRIEF Single word with surrounding whitespace is trimmed to the word.\n def test_single_word(self):\n assert normalize_whitespace(\" hello \") == \"hello\"\n # endregion test_single_word\n\n# endregion TestNormalizeWhitespace\n\n\n# region TestTruncateText [TYPE Class]\n# @BRIEF Test suite for truncate_text.\nclass TestTruncateText:\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string stays empty regardless of max_length.\n def test_empty_string(self):\n assert truncate_text(\"\") == \"\"\n assert truncate_text(\"\", max_length=0) == \"\"\n # endregion test_empty_string\n\n # region test_shorter_than_max [TYPE Function]\n # @BRIEF Text shorter than max_length is returned unchanged.\n def test_shorter_than_max(self):\n assert truncate_text(\"hi\", max_length=5) == \"hi\"\n # endregion test_shorter_than_max\n\n # region test_exactly_at_max [TYPE Function]\n # @BRIEF Text exactly at max_length is returned unchanged (no \"...\" appended).\n def test_exactly_at_max(self):\n assert truncate_text(\"hello\", max_length=5) == \"hello\"\n # endregion test_exactly_at_max\n\n # region test_longer_than_max [TYPE Function]\n # @BRIEF Text longer than max_length is truncated with \"...\" appended.\n def test_longer_than_max(self):\n result = truncate_text(\"hello world\", max_length=5)\n assert result == \"hello...\"\n assert len(result) == 8 # 5 chars + \"...\"\n # endregion test_longer_than_max\n\n # region test_zero_max_length [TYPE Function]\n # @BRIEF max_length=0 truncates any non-empty text to \"...\".\n def test_zero_max_length(self):\n assert truncate_text(\"hello\", max_length=0) == \"...\"\n # endregion test_zero_max_length\n\n # region test_default_max_length [TYPE Function]\n # @BRIEF Default max_length of 500 does not truncate short text.\n def test_default_max_length(self):\n text = \"x\" * 100\n assert truncate_text(text) == text\n # endregion test_default_max_length\n\n # region test_truncation_at_default_boundary [TYPE Function]\n # @BRIEF Text exactly 500 chars is unchanged; 501 chars is truncated.\n def test_truncation_at_default_boundary(self):\n text_500 = \"x\" * 500\n text_501 = \"x\" * 501\n assert truncate_text(text_500) == text_500\n result = truncate_text(text_501)\n assert result == \"x\" * 500 + \"...\"\n assert len(result) == 503\n # endregion test_truncation_at_default_boundary\n\n# endregion TestTruncateText\n\n\n# region TestCleanText [TYPE Class]\n# @BRIEF Test suite for clean_text (combined normalize + truncate).\nclass TestCleanText:\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string returns empty string.\n def test_empty_string(self):\n assert clean_text(\"\") == \"\"\n # endregion test_empty_string\n\n # region test_whitespace_only [TYPE Function]\n # @BRIEF Whitespace-only input returns empty string.\n def test_whitespace_only(self):\n assert clean_text(\" \\n \\t \") == \"\"\n # endregion test_whitespace_only\n\n # region test_clean_and_short [TYPE Function]\n # @BRIEF Already clean, short text is returned unchanged.\n def test_clean_and_short(self):\n assert clean_text(\"hello world\") == \"hello world\"\n # endregion test_clean_and_short\n\n # region test_normalize_then_truncate [TYPE Function]\n # @BRIEF Whitespace is normalized before truncation — counting clean chars.\n def test_normalize_then_truncate(self):\n # \"hello world\" has 15 chars raw, normalized to \"hello world\" (11 chars)\n # With max_length=5, should truncate normalized text to \"hello...\"\n result = clean_text(\"hello world\", max_length=5)\n assert result == \"hello...\"\n # endregion test_normalize_then_truncate\n\n # region test_normalize_keeps_text_under_limit [TYPE Function]\n # @BRIEF Normalization reduces length enough that truncation is not needed.\n def test_normalize_keeps_text_under_limit(self):\n # \"A B\" has 4 chars raw, normalizes to \"A B\" (3 chars), still under 5\n result = clean_text(\"A B\", max_length=5)\n assert result == \"A B\"\n # endregion test_normalize_keeps_text_under_limit\n\n # region test_leading_whitespace_then_truncate [TYPE Function]\n # @BRIEF Leading whitespace is removed before truncation, avoiding wasted chars.\n def test_leading_whitespace_then_truncate(self):\n # \" hello world\" normalizes to \"hello world\" (11 chars)\n # With max_length=5: truncates to \"hello...\"\n result = clean_text(\" hello world\", max_length=5)\n assert result == \"hello...\"\n # endregion test_leading_whitespace_then_truncate\n\n # region test_default_max_length_passthrough [TYPE Function]\n # @BRIEF With default max_length=500, text under 500 after normalize is unchanged.\n def test_default_max_length_passthrough(self):\n text = \" hello world \"\n expected = \"hello world\"\n assert clean_text(text) == expected\n # endregion test_default_max_length_passthrough\n\n# endregion TestCleanText\n# #endregion TestTextCleaner\n" + "body": "# #region TestTextCleaner [C:3] [TYPE Module] [SEMANTICS test, text, cleaner, whitespace, truncation]\n# @BRIEF Verify text cleaning contracts — normalize_whitespace, truncate_text, clean_text.\n# @RELATION BINDS_TO -> [TextCleaner]\n# @TEST_EDGE empty_string — empty/whitespace-only input returns \"\"\n# @TEST_EDGE whitespace_variants — multiple spaces, newlines, tabs all collapse to single space\n# @TEST_EDGE truncation_boundary — text shorter than, equal to, and longer than max_length\n# @TEST_EDGE clean_combined — clean_text correctly chains normalize + truncate\n# @TEST_EDGE zero_max_length — max_length=0 forces truncation on any non-empty text\n\nfrom src.plugins.translate._text_cleaner import clean_text, normalize_whitespace, truncate_text\n\n\n# region TestNormalizeWhitespace [TYPE Class]\n# @BRIEF Test suite for normalize_whitespace.\nclass TestNormalizeWhitespace:\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string returns empty string.\n def test_empty_string(self):\n assert normalize_whitespace(\"\") == \"\"\n # endregion test_empty_string\n\n # region test_whitespace_only [TYPE Function]\n # @BRIEF String with only whitespace returns empty string.\n def test_whitespace_only(self):\n assert normalize_whitespace(\" \") == \"\"\n assert normalize_whitespace(\"\\t\") == \"\"\n assert normalize_whitespace(\"\\n\") == \"\"\n # endregion test_whitespace_only\n\n # region test_multiple_spaces [TYPE Function]\n # @BRIEF Multiple spaces between words are collapsed to single space.\n def test_multiple_spaces(self):\n assert normalize_whitespace(\"hello world\") == \"hello world\"\n # endregion test_multiple_spaces\n\n # region test_multiple_newlines [TYPE Function]\n # @BRIEF Multiple newlines are collapsed to single space.\n def test_multiple_newlines(self):\n assert normalize_whitespace(\"hello\\n\\n\\nworld\") == \"hello world\"\n # endregion test_multiple_newlines\n\n # region test_mixed_whitespace [TYPE Function]\n # @BRIEF Tabs, newlines, and spaces all collapse to single space.\n def test_mixed_whitespace(self):\n assert normalize_whitespace(\"hello\\t \\n world\") == \"hello world\"\n # endregion test_mixed_whitespace\n\n # region test_leading_trailing_whitespace [TYPE Function]\n # @BRIEF Leading and trailing whitespace is trimmed.\n def test_leading_trailing_whitespace(self):\n assert normalize_whitespace(\" hello world \") == \"hello world\"\n assert normalize_whitespace(\"\\nhello world\\n\") == \"hello world\"\n # endregion test_leading_trailing_whitespace\n\n # region test_already_clean [TYPE Function]\n # @BRIEF Already clean text is returned unchanged.\n def test_already_clean(self):\n assert normalize_whitespace(\"hello world\") == \"hello world\"\n # endregion test_already_clean\n\n # region test_single_word [TYPE Function]\n # @BRIEF Single word with surrounding whitespace is trimmed to the word.\n def test_single_word(self):\n assert normalize_whitespace(\" hello \") == \"hello\"\n # endregion test_single_word\n\n# endregion TestNormalizeWhitespace\n\n\n# region TestTruncateText [TYPE Class]\n# @BRIEF Test suite for truncate_text.\nclass TestTruncateText:\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string stays empty regardless of max_length.\n def test_empty_string(self):\n assert truncate_text(\"\") == \"\"\n assert truncate_text(\"\", max_length=0) == \"\"\n # endregion test_empty_string\n\n # region test_shorter_than_max [TYPE Function]\n # @BRIEF Text shorter than max_length is returned unchanged.\n def test_shorter_than_max(self):\n assert truncate_text(\"hi\", max_length=5) == \"hi\"\n # endregion test_shorter_than_max\n\n # region test_exactly_at_max [TYPE Function]\n # @BRIEF Text exactly at max_length is returned unchanged (no \"...\" appended).\n def test_exactly_at_max(self):\n assert truncate_text(\"hello\", max_length=5) == \"hello\"\n # endregion test_exactly_at_max\n\n # region test_longer_than_max [TYPE Function]\n # @BRIEF Text longer than max_length is truncated with \"...\" appended.\n def test_longer_than_max(self):\n result = truncate_text(\"hello world\", max_length=5)\n assert result == \"hello...\"\n assert len(result) == 8 # 5 chars + \"...\"\n # endregion test_longer_than_max\n\n # region test_zero_max_length [TYPE Function]\n # @BRIEF max_length=0 truncates any non-empty text to \"...\".\n def test_zero_max_length(self):\n assert truncate_text(\"hello\", max_length=0) == \"...\"\n # endregion test_zero_max_length\n\n # region test_default_max_length [TYPE Function]\n # @BRIEF Default max_length of 500 does not truncate short text.\n def test_default_max_length(self):\n text = \"x\" * 100\n assert truncate_text(text) == text\n # endregion test_default_max_length\n\n # region test_truncation_at_default_boundary [TYPE Function]\n # @BRIEF Text exactly 500 chars is unchanged; 501 chars is truncated.\n def test_truncation_at_default_boundary(self):\n text_500 = \"x\" * 500\n text_501 = \"x\" * 501\n assert truncate_text(text_500) == text_500\n result = truncate_text(text_501)\n assert result == \"x\" * 500 + \"...\"\n assert len(result) == 503\n # endregion test_truncation_at_default_boundary\n\n# endregion TestTruncateText\n\n\n# region TestCleanText [TYPE Class]\n# @BRIEF Test suite for clean_text (combined normalize + truncate).\nclass TestCleanText:\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string returns empty string.\n def test_empty_string(self):\n assert clean_text(\"\") == \"\"\n # endregion test_empty_string\n\n # region test_whitespace_only [TYPE Function]\n # @BRIEF Whitespace-only input returns empty string.\n def test_whitespace_only(self):\n assert clean_text(\" \\n \\t \") == \"\"\n # endregion test_whitespace_only\n\n # region test_clean_and_short [TYPE Function]\n # @BRIEF Already clean, short text is returned unchanged.\n def test_clean_and_short(self):\n assert clean_text(\"hello world\") == \"hello world\"\n # endregion test_clean_and_short\n\n # region test_normalize_then_truncate [TYPE Function]\n # @BRIEF Whitespace is normalized before truncation — counting clean chars.\n def test_normalize_then_truncate(self):\n # \"hello world\" has 15 chars raw, normalized to \"hello world\" (11 chars)\n # With max_length=5, should truncate normalized text to \"hello...\"\n result = clean_text(\"hello world\", max_length=5)\n assert result == \"hello...\"\n # endregion test_normalize_then_truncate\n\n # region test_normalize_keeps_text_under_limit [TYPE Function]\n # @BRIEF Normalization reduces length enough that truncation is not needed.\n def test_normalize_keeps_text_under_limit(self):\n # \"A B\" has 4 chars raw, normalizes to \"A B\" (3 chars), still under 5\n result = clean_text(\"A B\", max_length=5)\n assert result == \"A B\"\n # endregion test_normalize_keeps_text_under_limit\n\n # region test_leading_whitespace_then_truncate [TYPE Function]\n # @BRIEF Leading whitespace is removed before truncation, avoiding wasted chars.\n def test_leading_whitespace_then_truncate(self):\n # \" hello world\" normalizes to \"hello world\" (11 chars)\n # With max_length=5: truncates to \"hello...\"\n result = clean_text(\" hello world\", max_length=5)\n assert result == \"hello...\"\n # endregion test_leading_whitespace_then_truncate\n\n # region test_default_max_length_passthrough [TYPE Function]\n # @BRIEF With default max_length=500, text under 500 after normalize is unchanged.\n def test_default_max_length_passthrough(self):\n text = \" hello world \"\n expected = \"hello world\"\n assert clean_text(text) == expected\n # endregion test_default_max_length_passthrough\n\n# endregion TestCleanText\n# #endregion TestTextCleaner\n" }, { "contract_id": "TestTokenBudget", @@ -36236,7 +36346,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 .[EXT:internal:_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, 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" }, { "contract_id": "process_batch", @@ -36275,7 +36385,7 @@ "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 .[EXT:internal:_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\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" }, { "contract_id": "resolve_provider_model", @@ -36479,7 +36589,7 @@ "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 .[EXT:internal:_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 ._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" }, { "contract_id": "call_llm_for_batch", @@ -37253,7 +37363,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryCorrectionService [C:3] [TYPE Class] [SEMANTICS dictionary, correction, bulk]\n# @BRIEF Submit term corrections and bulk corrections to dictionaries with conflict detection.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n\nfrom typing import Any\n\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom .[EXT:internal:_utils] import _normalize_term\n\n\nclass DictionaryCorrectionService:\n \"\"\"Submit term corrections to dictionaries with conflict detection and bulk support.\"\"\"\n\n # region submit_correction [TYPE Function]\n # @PURPOSE: Submit a term correction from a run result.\n @staticmethod\n def submit_correction(\n db: Session,\n dict_id: str,\n source_term: str,\n incorrect_target_term: str,\n corrected_target_term: str,\n origin_run_id: str | None = None,\n origin_row_key: str | None = None,\n origin_user_id: str | None = None,\n on_conflict: str = \"overwrite\",\n context_data: dict[str, Any] | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict[str, Any]:\n with belief_scope(\"DictionaryCorrectionService.submit_correction\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary '{dict_id}' not found\")\n\n effective_context = context_data\n effective_context_source = \"auto\"\n if not keep_context:\n effective_context = None\n effective_context_source = \"manual\"\n\n normalized = _normalize_term(source_term)\n entry_src_lang = dictionary.source_dialect or \"und\"\n entry_tgt_lang = dictionary.target_dialect or \"und\"\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n or_(\n (DictionaryEntry.source_language == entry_src_lang) & (DictionaryEntry.target_language == entry_tgt_lang),\n (DictionaryEntry.source_language == \"und\") & (DictionaryEntry.target_language == \"und\"),\n ),\n )\n .first()\n )\n\n result: dict[str, Any] = {\"source_term\": source_term, \"target_term\": corrected_target_term, \"action\": \"created\", \"entry_id\": None, \"conflict\": None, \"message\": None}\n\n if existing:\n if on_conflict == \"keep_existing\":\n result[\"action\"] = \"conflict_detected\"\n result[\"conflict\"] = {\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target_term, \"action\": \"keep_existing\"}\n result[\"message\"] = f\"Existing entry kept: '{existing.target_term}'\"\n return result\n elif on_conflict == \"overwrite\":\n existing.target_term = corrected_target_term.strip()\n if origin_run_id:\n existing.origin_run_id = origin_run_id\n if origin_row_key:\n existing.origin_row_key = origin_row_key\n if origin_user_id:\n existing.origin_user_id = origin_user_id\n if context_data is not None or not keep_context:\n existing.context_data = effective_context\n existing.has_context = bool(effective_context)\n existing.context_source = effective_context_source\n if usage_notes is not None:\n existing.usage_notes = usage_notes\n db.flush()\n result[\"action\"] = \"updated\"\n result[\"entry_id\"] = existing.id\n result[\"message\"] = f\"Entry updated from '{existing.target_term}' to '{corrected_target_term}'\"\n else:\n result[\"action\"] = \"skipped\"\n result[\"conflict\"] = {\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target_term, \"action\": \"cancel\"}\n result[\"message\"] = \"Correction cancelled by conflict mode\"\n return result\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id, source_term=source_term.strip(),\n source_term_normalized=normalized, target_term=corrected_target_term.strip(),\n source_language=entry_src_lang, target_language=entry_tgt_lang,\n context_data=effective_context, usage_notes=usage_notes,\n has_context=bool(effective_context), context_source=effective_context_source if effective_context else None,\n origin_run_id=origin_run_id, origin_row_key=origin_row_key, origin_user_id=origin_user_id,\n )\n db.add(entry)\n db.flush()\n result[\"entry_id\"] = entry.id\n result[\"message\"] = f\"Entry created for '{source_term}' -> '{corrected_target_term}'\"\n\n db.commit()\n logger.reflect(\"Correction processed\", result)\n return result\n # endregion submit_correction\n\n # region submit_bulk_corrections [TYPE Function]\n # @PURPOSE: Submit multiple term corrections atomically.\n @staticmethod\n def submit_bulk_corrections(\n db: Session,\n dict_id: str,\n corrections: list[dict[str, Any]],\n origin_user_id: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"DictionaryCorrectionService.submit_bulk_corrections\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary '{dict_id}' not found\")\n\n results: list[dict[str, Any]] = []\n conflicts: list[dict[str, Any]] = []\n all_ok = True\n\n for corr in corrections:\n source_term = corr.get(\"source_term\", \"\").strip()\n corrected_target = corr.get(\"corrected_target_term\", \"\").strip()\n\n if not source_term or not corrected_target:\n results.append({\"source_term\": source_term, \"action\": \"error\", \"message\": \"source_term and corrected_target_term are required\"})\n all_ok = False\n continue\n\n normalized = _normalize_term(source_term)\n bulk_src_lang = dictionary.source_dialect or \"und\"\n bulk_tgt_lang = dictionary.target_dialect or \"und\"\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n or_(\n (DictionaryEntry.source_language == bulk_src_lang) & (DictionaryEntry.target_language == bulk_tgt_lang),\n (DictionaryEntry.source_language == \"und\") & (DictionaryEntry.target_language == \"und\"),\n ),\n )\n .first()\n )\n\n if existing:\n on_conflict = corr.get(\"on_conflict\", \"overwrite\")\n if on_conflict == \"keep_existing\":\n conflicts.append({\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target, \"action\": \"keep_existing\"})\n results.append({\"source_term\": source_term, \"action\": \"conflict_detected\", \"message\": f\"Existing entry kept: '{existing.target_term}'\"})\n continue\n elif on_conflict == \"overwrite\":\n existing.target_term = corrected_target\n existing.origin_user_id = origin_user_id\n results.append({\"source_term\": source_term, \"action\": \"updated\", \"entry_id\": existing.id, \"message\": f\"Updated to '{corrected_target}'\"})\n else:\n conflicts.append({\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target, \"action\": \"cancel\"})\n results.append({\"source_term\": source_term, \"action\": \"skipped\", \"message\": \"Cancelled by conflict mode\"})\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id, source_term=source_term, source_term_normalized=normalized,\n target_term=corrected_target, source_language=bulk_src_lang, target_language=bulk_tgt_lang,\n origin_run_id=corr.get(\"origin_run_id\"), origin_row_key=corr.get(\"origin_row_key\"),\n origin_user_id=origin_user_id,\n )\n db.add(entry)\n results.append({\"source_term\": source_term, \"action\": \"created\", \"message\": f\"Entry created for '{source_term}' -> '{corrected_target}'\"})\n\n if conflicts and not all_ok:\n db.rollback()\n return {\"status\": \"conflicts\", \"results\": results, \"conflicts\": conflicts, \"message\": \"Conflicts detected. Resolve before retrying.\"}\n\n db.commit()\n return {\n \"status\": \"completed\", \"results\": results, \"conflicts\": conflicts,\n \"total\": len(corrections), \"applied\": sum(1 for r in results if r[\"action\"] in (\"created\", \"updated\")),\n }\n # endregion submit_bulk_corrections\n\n# #endregion DictionaryCorrectionService\n" + "body": "# #region DictionaryCorrectionService [C:3] [TYPE Class] [SEMANTICS dictionary, correction, bulk]\n# @BRIEF Submit term corrections and bulk corrections to dictionaries with conflict detection.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n\nfrom typing import Any\n\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom ._utils import _normalize_term\n\n\nclass DictionaryCorrectionService:\n \"\"\"Submit term corrections to dictionaries with conflict detection and bulk support.\"\"\"\n\n # region submit_correction [TYPE Function]\n # @PURPOSE: Submit a term correction from a run result.\n @staticmethod\n def submit_correction(\n db: Session,\n dict_id: str,\n source_term: str,\n incorrect_target_term: str,\n corrected_target_term: str,\n origin_run_id: str | None = None,\n origin_row_key: str | None = None,\n origin_user_id: str | None = None,\n on_conflict: str = \"overwrite\",\n context_data: dict[str, Any] | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict[str, Any]:\n with belief_scope(\"DictionaryCorrectionService.submit_correction\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary '{dict_id}' not found\")\n\n effective_context = context_data\n effective_context_source = \"auto\"\n if not keep_context:\n effective_context = None\n effective_context_source = \"manual\"\n\n normalized = _normalize_term(source_term)\n entry_src_lang = dictionary.source_dialect or \"und\"\n entry_tgt_lang = dictionary.target_dialect or \"und\"\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n or_(\n (DictionaryEntry.source_language == entry_src_lang) & (DictionaryEntry.target_language == entry_tgt_lang),\n (DictionaryEntry.source_language == \"und\") & (DictionaryEntry.target_language == \"und\"),\n ),\n )\n .first()\n )\n\n result: dict[str, Any] = {\"source_term\": source_term, \"target_term\": corrected_target_term, \"action\": \"created\", \"entry_id\": None, \"conflict\": None, \"message\": None}\n\n if existing:\n if on_conflict == \"keep_existing\":\n result[\"action\"] = \"conflict_detected\"\n result[\"conflict\"] = {\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target_term, \"action\": \"keep_existing\"}\n result[\"message\"] = f\"Existing entry kept: '{existing.target_term}'\"\n return result\n elif on_conflict == \"overwrite\":\n existing.target_term = corrected_target_term.strip()\n if origin_run_id:\n existing.origin_run_id = origin_run_id\n if origin_row_key:\n existing.origin_row_key = origin_row_key\n if origin_user_id:\n existing.origin_user_id = origin_user_id\n if context_data is not None or not keep_context:\n existing.context_data = effective_context\n existing.has_context = bool(effective_context)\n existing.context_source = effective_context_source\n if usage_notes is not None:\n existing.usage_notes = usage_notes\n db.flush()\n result[\"action\"] = \"updated\"\n result[\"entry_id\"] = existing.id\n result[\"message\"] = f\"Entry updated from '{existing.target_term}' to '{corrected_target_term}'\"\n else:\n result[\"action\"] = \"skipped\"\n result[\"conflict\"] = {\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target_term, \"action\": \"cancel\"}\n result[\"message\"] = \"Correction cancelled by conflict mode\"\n return result\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id, source_term=source_term.strip(),\n source_term_normalized=normalized, target_term=corrected_target_term.strip(),\n source_language=entry_src_lang, target_language=entry_tgt_lang,\n context_data=effective_context, usage_notes=usage_notes,\n has_context=bool(effective_context), context_source=effective_context_source if effective_context else None,\n origin_run_id=origin_run_id, origin_row_key=origin_row_key, origin_user_id=origin_user_id,\n )\n db.add(entry)\n db.flush()\n result[\"entry_id\"] = entry.id\n result[\"message\"] = f\"Entry created for '{source_term}' -> '{corrected_target_term}'\"\n\n db.commit()\n logger.reflect(\"Correction processed\", result)\n return result\n # endregion submit_correction\n\n # region submit_bulk_corrections [TYPE Function]\n # @PURPOSE: Submit multiple term corrections atomically.\n @staticmethod\n def submit_bulk_corrections(\n db: Session,\n dict_id: str,\n corrections: list[dict[str, Any]],\n origin_user_id: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"DictionaryCorrectionService.submit_bulk_corrections\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary '{dict_id}' not found\")\n\n results: list[dict[str, Any]] = []\n conflicts: list[dict[str, Any]] = []\n all_ok = True\n\n for corr in corrections:\n source_term = corr.get(\"source_term\", \"\").strip()\n corrected_target = corr.get(\"corrected_target_term\", \"\").strip()\n\n if not source_term or not corrected_target:\n results.append({\"source_term\": source_term, \"action\": \"error\", \"message\": \"source_term and corrected_target_term are required\"})\n all_ok = False\n continue\n\n normalized = _normalize_term(source_term)\n bulk_src_lang = dictionary.source_dialect or \"und\"\n bulk_tgt_lang = dictionary.target_dialect or \"und\"\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n or_(\n (DictionaryEntry.source_language == bulk_src_lang) & (DictionaryEntry.target_language == bulk_tgt_lang),\n (DictionaryEntry.source_language == \"und\") & (DictionaryEntry.target_language == \"und\"),\n ),\n )\n .first()\n )\n\n if existing:\n on_conflict = corr.get(\"on_conflict\", \"overwrite\")\n if on_conflict == \"keep_existing\":\n conflicts.append({\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target, \"action\": \"keep_existing\"})\n results.append({\"source_term\": source_term, \"action\": \"conflict_detected\", \"message\": f\"Existing entry kept: '{existing.target_term}'\"})\n continue\n elif on_conflict == \"overwrite\":\n existing.target_term = corrected_target\n existing.origin_user_id = origin_user_id\n results.append({\"source_term\": source_term, \"action\": \"updated\", \"entry_id\": existing.id, \"message\": f\"Updated to '{corrected_target}'\"})\n else:\n conflicts.append({\"source_term\": source_term, \"existing_target_term\": existing.target_term, \"submitted_target_term\": corrected_target, \"action\": \"cancel\"})\n results.append({\"source_term\": source_term, \"action\": \"skipped\", \"message\": \"Cancelled by conflict mode\"})\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id, source_term=source_term, source_term_normalized=normalized,\n target_term=corrected_target, source_language=bulk_src_lang, target_language=bulk_tgt_lang,\n origin_run_id=corr.get(\"origin_run_id\"), origin_row_key=corr.get(\"origin_row_key\"),\n origin_user_id=origin_user_id,\n )\n db.add(entry)\n results.append({\"source_term\": source_term, \"action\": \"created\", \"message\": f\"Entry created for '{source_term}' -> '{corrected_target}'\"})\n\n if conflicts and not all_ok:\n db.rollback()\n return {\"status\": \"conflicts\", \"results\": results, \"conflicts\": conflicts, \"message\": \"Conflicts detected. Resolve before retrying.\"}\n\n db.commit()\n return {\n \"status\": \"completed\", \"results\": results, \"conflicts\": conflicts,\n \"total\": len(corrections), \"applied\": sum(1 for r in results if r[\"action\"] in (\"created\", \"updated\")),\n }\n # endregion submit_bulk_corrections\n\n# #endregion DictionaryCorrectionService\n" }, { "contract_id": "DictionaryCRUD", @@ -37321,7 +37431,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryEntryCRUD [C:3] [TYPE Class] [SEMANTICS dictionary, entries, crud]\n# @BRIEF CRUD operations for DictionaryEntry records.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom .[EXT:internal:_utils] import _normalize_term\nfrom .dictionary_validation import _validate_bcp47\n\n\nclass DictionaryEntryCRUD:\n \"\"\"CRUD operations for dictionary entries.\"\"\"\n\n # region add_entry [TYPE Function]\n # @PURPOSE: Add an entry to a dictionary with language-pair-aware uniqueness validation.\n @staticmethod\n def add_entry(\n db: Session, dict_id: str, source_term: str, target_term: str,\n context_notes: str | None = None,\n source_language: str = \"und\", target_language: str = \"und\",\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryEntryCRUD.add_entry\"):\n _validate_bcp47(source_language, \"source_language\")\n _validate_bcp47(target_language, \"target_language\")\n\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' (lang: {source_language}→{target_language}) \"\n f\"already exists in this dictionary (id={existing.id}). \"\n \"Use overwrite or keep_existing conflict mode.\"\n )\n\n logger.reason(\"Adding dictionary entry\", {\"dict_id\": dict_id, \"term\": source_term})\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=target_term.strip(),\n context_notes=context_notes.strip() if context_notes else None,\n source_language=source_language.strip(),\n target_language=target_language.strip(),\n )\n db.add(entry)\n db.commit()\n db.refresh(entry)\n logger.reflect(\"Entry added\", {\"entry_id\": entry.id})\n return entry\n # endregion add_entry\n\n # region edit_entry [TYPE Function]\n # @PURPOSE: Edit an existing dictionary entry with language-pair-aware duplicate check.\n @staticmethod\n def edit_entry(\n db: Session, entry_id: str, source_term: str | None = None,\n target_term: str | None = None, context_notes: str | None = None,\n source_language: str | None = None, target_language: str | None = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryEntryCRUD.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n logger.reason(\"Editing dictionary entry\", {\"entry_id\": entry_id})\n\n if source_language is not None:\n _validate_bcp47(source_language, \"source_language\")\n entry.source_language = source_language.strip()\n if target_language is not None:\n _validate_bcp47(target_language, \"target_language\")\n entry.target_language = target_language.strip()\n\n if source_term is not None:\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == entry.source_language,\n DictionaryEntry.target_language == entry.target_language,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(f\"Duplicate entry: '{source_term}' already exists in this dictionary (id={existing.id}).\")\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n logger.reflect(\"Entry updated\", {\"entry_id\": entry.id})\n return entry\n # endregion edit_entry\n\n # region delete_entry [TYPE Function]\n # @PURPOSE: Delete a single dictionary entry.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryEntryCRUD.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n logger.reason(\"Deleting dictionary entry\", {\"entry_id\": entry_id})\n db.delete(entry)\n db.commit()\n logger.reflect(\"Entry deleted\", {\"entry_id\": entry_id})\n # endregion delete_entry\n\n # region clear_entries [TYPE Function]\n # @PURPOSE: Delete all entries for a dictionary.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryEntryCRUD.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n logger.reason(\"Clearing all entries\", {\"dict_id\": dict_id})\n deleted = db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.commit()\n logger.reflect(\"Entries cleared\", {\"dict_id\": dict_id, \"count\": deleted})\n return deleted\n # endregion clear_entries\n\n # region list_entries [TYPE Function]\n # @PURPOSE: List entries for a dictionary with pagination.\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> tuple[list[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar() or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # endregion list_entries\n\n# #endregion DictionaryEntryCRUD\n" + "body": "# #region DictionaryEntryCRUD [C:3] [TYPE Class] [SEMANTICS dictionary, entries, crud]\n# @BRIEF CRUD operations for DictionaryEntry records.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom ._utils import _normalize_term\nfrom .dictionary_validation import _validate_bcp47\n\n\nclass DictionaryEntryCRUD:\n \"\"\"CRUD operations for dictionary entries.\"\"\"\n\n # region add_entry [TYPE Function]\n # @PURPOSE: Add an entry to a dictionary with language-pair-aware uniqueness validation.\n @staticmethod\n def add_entry(\n db: Session, dict_id: str, source_term: str, target_term: str,\n context_notes: str | None = None,\n source_language: str = \"und\", target_language: str = \"und\",\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryEntryCRUD.add_entry\"):\n _validate_bcp47(source_language, \"source_language\")\n _validate_bcp47(target_language, \"target_language\")\n\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' (lang: {source_language}→{target_language}) \"\n f\"already exists in this dictionary (id={existing.id}). \"\n \"Use overwrite or keep_existing conflict mode.\"\n )\n\n logger.reason(\"Adding dictionary entry\", {\"dict_id\": dict_id, \"term\": source_term})\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=target_term.strip(),\n context_notes=context_notes.strip() if context_notes else None,\n source_language=source_language.strip(),\n target_language=target_language.strip(),\n )\n db.add(entry)\n db.commit()\n db.refresh(entry)\n logger.reflect(\"Entry added\", {\"entry_id\": entry.id})\n return entry\n # endregion add_entry\n\n # region edit_entry [TYPE Function]\n # @PURPOSE: Edit an existing dictionary entry with language-pair-aware duplicate check.\n @staticmethod\n def edit_entry(\n db: Session, entry_id: str, source_term: str | None = None,\n target_term: str | None = None, context_notes: str | None = None,\n source_language: str | None = None, target_language: str | None = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryEntryCRUD.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n logger.reason(\"Editing dictionary entry\", {\"entry_id\": entry_id})\n\n if source_language is not None:\n _validate_bcp47(source_language, \"source_language\")\n entry.source_language = source_language.strip()\n if target_language is not None:\n _validate_bcp47(target_language, \"target_language\")\n entry.target_language = target_language.strip()\n\n if source_term is not None:\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == entry.source_language,\n DictionaryEntry.target_language == entry.target_language,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(f\"Duplicate entry: '{source_term}' already exists in this dictionary (id={existing.id}).\")\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n logger.reflect(\"Entry updated\", {\"entry_id\": entry.id})\n return entry\n # endregion edit_entry\n\n # region delete_entry [TYPE Function]\n # @PURPOSE: Delete a single dictionary entry.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryEntryCRUD.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n logger.reason(\"Deleting dictionary entry\", {\"entry_id\": entry_id})\n db.delete(entry)\n db.commit()\n logger.reflect(\"Entry deleted\", {\"entry_id\": entry_id})\n # endregion delete_entry\n\n # region clear_entries [TYPE Function]\n # @PURPOSE: Delete all entries for a dictionary.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryEntryCRUD.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n logger.reason(\"Clearing all entries\", {\"dict_id\": dict_id})\n deleted = db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.commit()\n logger.reflect(\"Entries cleared\", {\"dict_id\": dict_id, \"count\": deleted})\n return deleted\n # endregion clear_entries\n\n # region list_entries [TYPE Function]\n # @PURPOSE: List entries for a dictionary with pagination.\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> tuple[list[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar() or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # endregion list_entries\n\n# #endregion DictionaryEntryCRUD\n" }, { "contract_id": "DictionaryBatchFilter", @@ -37395,7 +37505,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryImportExport [C:3] [TYPE Class] [SEMANTICS dictionary, import, export, csv, migration]\n# @BRIEF Import/export entries as CSV/TSV with conflict detection, and migration of old entries.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n\nimport csv\nimport io\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom .[EXT:internal:_utils] import _detect_delimiter, _normalize_term\n\n\nclass DictionaryImportExport:\n \"\"\"Import/export dictionary entries as CSV/TSV, and migrate old-style entries.\"\"\"\n\n # region import_entries [TYPE Function]\n # @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution.\n @staticmethod\n def import_entries(\n db: Session, dict_id: str, content: str,\n delimiter: str | None = None,\n on_conflict: str = \"overwrite\",\n preview_only: bool = False,\n default_source_language: str | None = None,\n default_target_language: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"DictionaryImportExport.import_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n if not delimiter:\n delimiter = _detect_delimiter(content)\n logger.reason(\"Detected delimiter\", {\"delimiter\": repr(delimiter)})\n\n if delimiter not in (\",\", \"\\t\"):\n raise ValueError(f\"Unsupported delimiter: {delimiter!r}. Use ',' or '\\\\t'.\")\n\n reader = csv.DictReader(io.StringIO(content), delimiter=delimiter)\n required_fields = {\"source_term\", \"target_term\"}\n if not reader.fieldnames or not required_fields.issubset(reader.fieldnames):\n raise ValueError(\n f\"CSV/TSV must have at least 'source_term' and 'target_term' columns. \"\n f\"Got: {reader.fieldnames}\"\n )\n\n result: dict[str, Any] = {\"total\": 0, \"created\": 0, \"updated\": 0, \"skipped\": 0, \"errors\": [], \"preview\": []}\n rows = list(reader)\n result[\"total\"] = len(rows)\n\n for row_idx, row in enumerate(rows):\n try:\n source_term = row.get(\"source_term\", \"\").strip()\n target_term = row.get(\"target_term\", \"\").strip()\n context_notes = row.get(\"context_notes\", \"\").strip() or None\n source_language = row.get(\"source_language\", \"\").strip() or default_source_language or \"und\"\n target_language = row.get(\"target_language\", \"\").strip() or default_target_language or \"und\"\n\n if not source_term or not target_term:\n result[\"errors\"].append({\"line\": row_idx + 2, \"error\": \"Both source_term and target_term are required\", \"row\": row})\n continue\n\n normalized = _normalize_term(source_term)\n\n if preview_only:\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n result[\"preview\"].append({\n \"line\": row_idx + 2, \"source_term\": source_term, \"target_term\": target_term,\n \"context_notes\": context_notes, \"source_language\": source_language,\n \"target_language\": target_language, \"is_conflict\": existing is not None,\n \"existing_target_term\": existing.target_term if existing else None,\n })\n continue\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_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 if existing:\n if on_conflict == \"overwrite\":\n existing.source_term = source_term\n existing.target_term = target_term\n existing.context_notes = context_notes\n existing.source_language = source_language\n existing.target_language = target_language\n result[\"updated\"] += 1\n elif on_conflict == \"keep_existing\":\n result[\"skipped\"] += 1\n else:\n result[\"errors\"].append({\"line\": row_idx + 2, \"error\": f\"Conflict on '{source_term}' and on_conflict='cancel'\", \"row\": row})\n continue\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id, source_term=source_term,\n source_term_normalized=normalized, target_term=target_term,\n context_notes=context_notes, source_language=source_language,\n target_language=target_language,\n )\n db.add(entry)\n result[\"created\"] += 1\n except Exception as e:\n result[\"errors\"].append({\"line\": row_idx + 2, \"error\": str(e), \"row\": row})\n\n if not preview_only:\n db.commit()\n\n logger.reflect(\"Import complete\", {\"dict_id\": dict_id, \"total\": result[\"total\"], \"created\": result[\"created\"]})\n return result\n # endregion import_entries\n\n # region export_entries [TYPE Function]\n # @PURPOSE: Export all entries as CSV string with language columns.\n @staticmethod\n def export_entries(db: Session, dict_id: str, delimiter: str = \",\") -> str:\n with belief_scope(\"DictionaryImportExport.export_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .all()\n )\n\n output = io.StringIO()\n fieldnames = [\"source_term\", \"target_term\", \"source_language\", \"target_language\", \"context_notes\", \"context_data\", \"usage_notes\"]\n writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=delimiter)\n writer.writeheader()\n for entry in entries:\n writer.writerow({\n \"source_term\": entry.source_term, \"target_term\": entry.target_term,\n \"source_language\": entry.source_language, \"target_language\": entry.target_language,\n \"context_notes\": entry.context_notes or \"\", \"context_data\": entry.context_data,\n \"usage_notes\": entry.usage_notes or \"\",\n })\n\n result = output.getvalue()\n logger.reflect(\"Entries exported\", {\"dict_id\": dict_id, \"count\": len(entries)})\n return result\n # endregion export_entries\n\n # region migrate_old_entries [TYPE Function]\n # @PURPOSE: Migrate existing single-language dictionary entries to populate source_language and target_language.\n @staticmethod\n def migrate_old_entries(db: Session) -> dict[str, int]:\n with belief_scope(\"DictionaryImportExport.migrate_old_entries\"):\n logger.reason(\"Starting migration of old dictionary entries\")\n migrated_source = 0\n migrated_target = 0\n skipped = 0\n\n entries = (\n db.query(DictionaryEntry)\n .filter(\n (DictionaryEntry.source_language == \"und\") |\n (DictionaryEntry.target_language == \"und\")\n )\n .all()\n )\n\n for entry in entries:\n if entry.source_language == \"und\":\n if entry.origin_source_language:\n entry.source_language = entry.origin_source_language\n migrated_source += 1\n if entry.source_language == \"und\" and entry.target_language == \"und\":\n skipped += 1\n\n db.commit()\n logger.reflect(\"Migration complete\", {\"migrated_source\": migrated_source, \"total_processed\": len(entries)})\n return {\"migrated_source\": migrated_source, \"migrated_target\": migrated_target, \"skipped\": skipped, \"total_processed\": len(entries)}\n # endregion migrate_old_entries\n\n# #endregion DictionaryImportExport\n" + "body": "# #region DictionaryImportExport [C:3] [TYPE Class] [SEMANTICS dictionary, import, export, csv, migration]\n# @BRIEF Import/export entries as CSV/TSV with conflict detection, and migration of old entries.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n\nimport csv\nimport io\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom ._utils import _detect_delimiter, _normalize_term\n\n\nclass DictionaryImportExport:\n \"\"\"Import/export dictionary entries as CSV/TSV, and migrate old-style entries.\"\"\"\n\n # region import_entries [TYPE Function]\n # @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution.\n @staticmethod\n def import_entries(\n db: Session, dict_id: str, content: str,\n delimiter: str | None = None,\n on_conflict: str = \"overwrite\",\n preview_only: bool = False,\n default_source_language: str | None = None,\n default_target_language: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"DictionaryImportExport.import_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n if not delimiter:\n delimiter = _detect_delimiter(content)\n logger.reason(\"Detected delimiter\", {\"delimiter\": repr(delimiter)})\n\n if delimiter not in (\",\", \"\\t\"):\n raise ValueError(f\"Unsupported delimiter: {delimiter!r}. Use ',' or '\\\\t'.\")\n\n reader = csv.DictReader(io.StringIO(content), delimiter=delimiter)\n required_fields = {\"source_term\", \"target_term\"}\n if not reader.fieldnames or not required_fields.issubset(reader.fieldnames):\n raise ValueError(\n f\"CSV/TSV must have at least 'source_term' and 'target_term' columns. \"\n f\"Got: {reader.fieldnames}\"\n )\n\n result: dict[str, Any] = {\"total\": 0, \"created\": 0, \"updated\": 0, \"skipped\": 0, \"errors\": [], \"preview\": []}\n rows = list(reader)\n result[\"total\"] = len(rows)\n\n for row_idx, row in enumerate(rows):\n try:\n source_term = row.get(\"source_term\", \"\").strip()\n target_term = row.get(\"target_term\", \"\").strip()\n context_notes = row.get(\"context_notes\", \"\").strip() or None\n source_language = row.get(\"source_language\", \"\").strip() or default_source_language or \"und\"\n target_language = row.get(\"target_language\", \"\").strip() or default_target_language or \"und\"\n\n if not source_term or not target_term:\n result[\"errors\"].append({\"line\": row_idx + 2, \"error\": \"Both source_term and target_term are required\", \"row\": row})\n continue\n\n normalized = _normalize_term(source_term)\n\n if preview_only:\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n result[\"preview\"].append({\n \"line\": row_idx + 2, \"source_term\": source_term, \"target_term\": target_term,\n \"context_notes\": context_notes, \"source_language\": source_language,\n \"target_language\": target_language, \"is_conflict\": existing is not None,\n \"existing_target_term\": existing.target_term if existing else None,\n })\n continue\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_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 if existing:\n if on_conflict == \"overwrite\":\n existing.source_term = source_term\n existing.target_term = target_term\n existing.context_notes = context_notes\n existing.source_language = source_language\n existing.target_language = target_language\n result[\"updated\"] += 1\n elif on_conflict == \"keep_existing\":\n result[\"skipped\"] += 1\n else:\n result[\"errors\"].append({\"line\": row_idx + 2, \"error\": f\"Conflict on '{source_term}' and on_conflict='cancel'\", \"row\": row})\n continue\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id, source_term=source_term,\n source_term_normalized=normalized, target_term=target_term,\n context_notes=context_notes, source_language=source_language,\n target_language=target_language,\n )\n db.add(entry)\n result[\"created\"] += 1\n except Exception as e:\n result[\"errors\"].append({\"line\": row_idx + 2, \"error\": str(e), \"row\": row})\n\n if not preview_only:\n db.commit()\n\n logger.reflect(\"Import complete\", {\"dict_id\": dict_id, \"total\": result[\"total\"], \"created\": result[\"created\"]})\n return result\n # endregion import_entries\n\n # region export_entries [TYPE Function]\n # @PURPOSE: Export all entries as CSV string with language columns.\n @staticmethod\n def export_entries(db: Session, dict_id: str, delimiter: str = \",\") -> str:\n with belief_scope(\"DictionaryImportExport.export_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .all()\n )\n\n output = io.StringIO()\n fieldnames = [\"source_term\", \"target_term\", \"source_language\", \"target_language\", \"context_notes\", \"context_data\", \"usage_notes\"]\n writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=delimiter)\n writer.writeheader()\n for entry in entries:\n writer.writerow({\n \"source_term\": entry.source_term, \"target_term\": entry.target_term,\n \"source_language\": entry.source_language, \"target_language\": entry.target_language,\n \"context_notes\": entry.context_notes or \"\", \"context_data\": entry.context_data,\n \"usage_notes\": entry.usage_notes or \"\",\n })\n\n result = output.getvalue()\n logger.reflect(\"Entries exported\", {\"dict_id\": dict_id, \"count\": len(entries)})\n return result\n # endregion export_entries\n\n # region migrate_old_entries [TYPE Function]\n # @PURPOSE: Migrate existing single-language dictionary entries to populate source_language and target_language.\n @staticmethod\n def migrate_old_entries(db: Session) -> dict[str, int]:\n with belief_scope(\"DictionaryImportExport.migrate_old_entries\"):\n logger.reason(\"Starting migration of old dictionary entries\")\n migrated_source = 0\n migrated_target = 0\n skipped = 0\n\n entries = (\n db.query(DictionaryEntry)\n .filter(\n (DictionaryEntry.source_language == \"und\") |\n (DictionaryEntry.target_language == \"und\")\n )\n .all()\n )\n\n for entry in entries:\n if entry.source_language == \"und\":\n if entry.origin_source_language:\n entry.source_language = entry.origin_source_language\n migrated_source += 1\n if entry.source_language == \"und\" and entry.target_language == \"und\":\n skipped += 1\n\n db.commit()\n logger.reflect(\"Migration complete\", {\"migrated_source\": migrated_source, \"total_processed\": len(entries)})\n return {\"migrated_source\": migrated_source, \"migrated_target\": migrated_target, \"skipped\": skipped, \"total_processed\": len(entries)}\n # endregion migrate_old_entries\n\n# #endregion DictionaryImportExport\n" }, { "contract_id": "_validate_bcp47", @@ -37470,7 +37580,7 @@ "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 [EXT:internal:_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 .[EXT:internal:_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 ._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" }, { "contract_id": "TranslationMetrics", @@ -38230,7 +38340,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslatePlugin [C:2] [TYPE Module] [SEMANTICS clickhouse, translate, sql, dashboard, dialect]\n# @BRIEF TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.\n# @LAYER Domain\n# @RELATION INHERITS -> [PluginBase]\n# @RATIONALE Separate plugin avoids bloating LLMAnalysisPlugin beyond fractal limit (<400 lines).\n# @REJECTED Extending LLMAnalysisPlugin would conflate two distinct feature domains.\n\nfrom typing import Any\n\nfrom ...core.plugin_base import PluginBase\n\n\n# #region TranslatePlugin [TYPE Class]\n# @BRIEF Plugin for translating SQL queries and dashboard definitions across database dialects.\n# @RELATION IMPLEMENTS -> [EXT:path:backend.src.core.plugin_base.PluginBase]\nclass TranslatePlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"translate\"\n\n @property\n def name(self) -> str:\n return \"LLM Table Translation\"\n\n @property\n def description(self) -> str:\n return \"Cross-dialect SQL and dashboard translation using LLMs.\"\n\n @property\n def version(self) -> str:\n return \"1.0.0\"\n\n def get_schema(self) -> dict[str, Any]:\n return {\n \"type\": \"object\",\n \"properties\": {\n \"source_dialect\": {\n \"type\": \"string\",\n \"title\": \"Source Dialect\",\n \"description\": \"Source database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"target_dialect\": {\n \"type\": \"string\",\n \"title\": \"Target Dialect\",\n \"description\": \"Target database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"job_id\": {\n \"type\": \"string\",\n \"title\": \"Translation Job ID\",\n \"description\": \"Existing translation job to execute\",\n },\n },\n \"required\": [\"job_id\"],\n }\n\n async def execute(self, params: dict[str, Any], context: Any | None = None):\n \"\"\"Execute a translation job — implementation deferred to later phases.\"\"\"\n raise NotImplementedError(\"TranslatePlugin.execute not yet implemented\")\n# #endregion TranslatePlugin\n\n# #endregion TranslatePlugin\n" + "body": "# #region TranslatePlugin [C:2] [TYPE Module] [SEMANTICS clickhouse, translate, sql, dashboard, dialect]\n# @BRIEF TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.\n# @LAYER Domain\n# @RELATION INHERITS -> [PluginBase]\n# @RATIONALE Separate plugin avoids bloating LLMAnalysisPlugin beyond fractal limit (<400 lines).\n# @REJECTED Extending LLMAnalysisPlugin would conflate two distinct feature domains.\n\nfrom typing import Any\n\nfrom ...core.plugin_base import PluginBase\n\n\n# #region TranslatePlugin [TYPE Class]\n# @BRIEF Plugin for translating SQL queries and dashboard definitions across database dialects.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass TranslatePlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"translate\"\n\n @property\n def name(self) -> str:\n return \"LLM Table Translation\"\n\n @property\n def description(self) -> str:\n return \"Cross-dialect SQL and dashboard translation using LLMs.\"\n\n @property\n def version(self) -> str:\n return \"1.0.0\"\n\n def get_schema(self) -> dict[str, Any]:\n return {\n \"type\": \"object\",\n \"properties\": {\n \"source_dialect\": {\n \"type\": \"string\",\n \"title\": \"Source Dialect\",\n \"description\": \"Source database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"target_dialect\": {\n \"type\": \"string\",\n \"title\": \"Target Dialect\",\n \"description\": \"Target database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"job_id\": {\n \"type\": \"string\",\n \"title\": \"Translation Job ID\",\n \"description\": \"Existing translation job to execute\",\n },\n },\n \"required\": [\"job_id\"],\n }\n\n async def execute(self, params: dict[str, Any], context: Any | None = None):\n \"\"\"Execute a translation job — implementation deferred to later phases.\"\"\"\n raise NotImplementedError(\"TranslatePlugin.execute not yet implemented\")\n# #endregion TranslatePlugin\n\n# #endregion TranslatePlugin\n" }, { "contract_id": "DEFAULT_EXECUTION_PROMPT_TEMPLATE", @@ -38846,7 +38956,7 @@ "POST": "Translation jobs are created/updated/deleted with column validation and dialect caching.", "PRE": "Database session and config manager are available.", "RATIONALE": "Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.", - "REJECTED": "Monolithic service.py — violated INV_7. Decomposed into service_datasource, service_inline_correction, service_bulk_replace, service[EXT:internal:_utils].", + "REJECTED": "Monolithic service.py — violated INV_7. Decomposed into service_datasource, service_inline_correction, service_bulk_replace, service_utils.", "SIDE_EFFECT": "Queries Superset for column metadata and database dialect at save time." }, "relations": [ @@ -38872,7 +38982,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[EXT:internal:_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[EXT:internal:_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[EXT:internal:_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\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" }, { "contract_id": "BulkFindReplaceService", @@ -39043,7 +39153,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 .[EXT:internal:_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\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" }, { "contract_id": "TargetSchemaValidation", @@ -39528,4938 +39638,6 @@ "tier_source": "AutoCalculated", "body": "# #region test_dashboard_health_item_status_accepts_only_whitelisted_values [TYPE Function]\n# @RELATION BINDS_TO -> TestSettingsAndHealthSchemas\n# @BRIEF Verify strict grouped regex only accepts PASS/WARN/FAIL/UNKNOWN exact statuses.\ndef test_dashboard_health_item_status_accepts_only_whitelisted_values():\n valid = DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASS\",\n last_check=\"2026-03-10T10:00:00\",\n )\n assert valid.status == \"PASS\"\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASSING\",\n last_check=\"2026-03-10T10:00:00\",\n )\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"FAIL \",\n last_check=\"2026-03-10T10:00:00\",\n )\n# #endregion test_dashboard_health_item_status_accepts_only_whitelisted_values\n" }, - { - "contract_id": "ExternalStubs", - "contract_type": "Module", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1, - "end_line": 774, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Stub contract references for external libraries, frontend stores, Python stdlib,", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region ExternalStubs [C:1] [TYPE Module] [SEMANTICS external,stubs,contracts]\n# @BRIEF Stub contract references for external libraries, frontend stores, Python stdlib,\n# file paths, and other targets that exist outside the GRACE-Poly contract network.\n# @RATIONALE These are placeholder contracts that allow @RELATION edges to resolve.\n# The EXT: prefix categorizes the external target type (Python stdlib, Library,\n# frontend store, file path, method reference, etc.).\n# @REJECTED Leaving unresolved_relation warnings was rejected — every @RELATION edge\n# MUST point to a valid contract ID. These stubs provide the target.\n\n# #region EXT:FastAPI:TestClient [C:1] [TYPE External]\n# @BRIEF External reference: TestClient\n# #endregion EXT:FastAPI:TestClient\n\n# #region EXT:Library:SQLAlchemy.Base [C:1] [TYPE External]\n# @BRIEF External library: SQLAlchemy.Base\n# #endregion EXT:Library:SQLAlchemy.Base\n\n# #region EXT:Library:authlib [C:1] [TYPE External]\n# @BRIEF External library: authlib\n# #endregion EXT:Library:authlib\n\n# #region EXT:Library:fastapi.APIRouter [C:1] [TYPE External]\n# @BRIEF External library: fastapi.APIRouter\n# #endregion EXT:Library:fastapi.APIRouter\n\n# #region EXT:Library:pydantic [C:1] [TYPE External]\n# @BRIEF External library: pydantic\n# #endregion EXT:Library:pydantic\n\n# #region EXT:Library:pytest [C:1] [TYPE External]\n# @BRIEF External library: pytest\n# #endregion EXT:Library:pytest\n\n# #region EXT:Library:sqlalchemy [C:1] [TYPE External]\n# @BRIEF External library: sqlalchemy\n# #endregion EXT:Library:sqlalchemy\n\n# #region EXT:Library:sqlalchemy.orm.Session [C:1] [TYPE External]\n# @BRIEF External library: sqlalchemy.orm.Session\n# #endregion EXT:Library:sqlalchemy.orm.Session\n\n# #region EXT:Library:yaml [C:1] [TYPE External]\n# @BRIEF External library: yaml\n# #endregion EXT:Library:yaml\n\n# #region EXT:Python:Exception [C:1] [TYPE External]\n# @BRIEF Python standard library module: Exception\n# #endregion EXT:Python:Exception\n\n# #region EXT:Python:hashlib [C:1] [TYPE External]\n# @BRIEF Python standard library module: hashlib\n# #endregion EXT:Python:hashlib\n\n# #region EXT:Python:hashlib.sha256 [C:1] [TYPE External]\n# @BRIEF Python standard library module: hashlib.sha256\n# #endregion EXT:Python:hashlib.sha256\n\n# #region EXT:Python:json [C:1] [TYPE External]\n# @BRIEF Python standard library module: json\n# #endregion EXT:Python:json\n\n# #region EXT:Python:secrets [C:1] [TYPE External]\n# @BRIEF Python standard library module: secrets\n# #endregion EXT:Python:secrets\n\n# #region EXT:Python:secrets.token_urlsafe [C:1] [TYPE External]\n# @BRIEF Python standard library module: secrets.token_urlsafe\n# #endregion EXT:Python:secrets.token_urlsafe\n\n# #region EXT:Python:uuid [C:1] [TYPE External]\n# @BRIEF Python standard library module: uuid\n# #endregion EXT:Python:uuid\n\n# #region EXT:build.sh [C:1] [TYPE External]\n# @BRIEF External reference: build.sh\n# #endregion EXT:build.sh\n\n# #region EXT:code:has_permission(\"admin:settings\", \"WRITE\") [C:1] [TYPE External]\n# @BRIEF Code expression: has_permission(\"admin:settings\", \"WRITE\")\n# #endregion EXT:code:has_permission(\"admin:settings\", \"WRITE\")\n\n# #region EXT:code:has_permission(\"maintenance\", \"READ\") [C:1] [TYPE External]\n# @BRIEF Code expression: has_permission(\"maintenance\", \"READ\")\n# #endregion EXT:code:has_permission(\"maintenance\", \"READ\")\n\n# #region EXT:code:has_permission(\"maintenance\", \"WRITE\") [C:1] [TYPE External]\n# @BRIEF Code expression: has_permission(\"maintenance\", \"WRITE\")\n# #endregion EXT:code:has_permission(\"maintenance\", \"WRITE\")\n\n# #region EXT:frontend:AssistantChatPanel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: AssistantChatPanel\n# #endregion EXT:frontend:AssistantChatPanel\n\n# #region EXT:frontend:AssistantFirstMessageIntegrationTest [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: AssistantFirstMessageIntegrationTest\n# #endregion EXT:frontend:AssistantFirstMessageIntegrationTest\n\n# #region EXT:frontend:BranchSelector [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: BranchSelector\n# #endregion EXT:frontend:BranchSelector\n\n# #region EXT:frontend:Breadcrumbs [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: Breadcrumbs\n# #endregion EXT:frontend:Breadcrumbs\n\n# #region EXT:frontend:ConflictResolver [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ConflictResolver\n# #endregion EXT:frontend:ConflictResolver\n\n# #region EXT:frontend:DashboardHub [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: DashboardHub\n# #endregion EXT:frontend:DashboardHub\n\n# #region EXT:frontend:DashboardValidationService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: DashboardValidationService\n# #endregion EXT:frontend:DashboardValidationService\n\n# #region EXT:frontend:DeploymentModal [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: DeploymentModal\n# #endregion EXT:frontend:DeploymentModal\n\n# #region EXT:frontend:EnvironmentsTab [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: EnvironmentsTab\n# #endregion EXT:frontend:EnvironmentsTab\n\n# #region EXT:frontend:GitApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: GitApi\n# #endregion EXT:frontend:GitApi\n\n# #region EXT:frontend:GitSettingsPage [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: GitSettingsPage\n# #endregion EXT:frontend:GitSettingsPage\n\n# #region EXT:frontend:LocaleEn [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: LocaleEn\n# #endregion EXT:frontend:LocaleEn\n\n# #region EXT:frontend:LocaleRu [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: LocaleRu\n# #endregion EXT:frontend:LocaleRu\n\n# #region EXT:frontend:MaintenanceEventsTable [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: MaintenanceEventsTable\n# #endregion EXT:frontend:MaintenanceEventsTable\n\n# #region EXT:frontend:MaintenanceServiceModule [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: MaintenanceServiceModule\n# #endregion EXT:frontend:MaintenanceServiceModule\n\n# #region EXT:frontend:MaintenanceSettingsPanel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: MaintenanceSettingsPanel\n# #endregion EXT:frontend:MaintenanceSettingsPanel\n\n# #region EXT:frontend:PluginLoaderCore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: PluginLoaderCore\n# #endregion EXT:frontend:PluginLoaderCore\n\n# #region EXT:frontend:ProfilePageBindAccountFlowTests [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ProfilePageBindAccountFlowTests\n# #endregion EXT:frontend:ProfilePageBindAccountFlowTests\n\n# #region EXT:frontend:ProtectedRoute [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ProtectedRoute\n# #endregion EXT:frontend:ProtectedRoute\n\n# #region EXT:frontend:ProviderConfigIntegrationTest [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ProviderConfigIntegrationTest\n# #endregion EXT:frontend:ProviderConfigIntegrationTest\n\n# #region EXT:frontend:ReportCard [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportCard\n# #endregion EXT:frontend:ReportCard\n\n# #region EXT:frontend:ReportDetailPanel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportDetailPanel\n# #endregion EXT:frontend:ReportDetailPanel\n\n# #region EXT:frontend:ReportModel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportModel\n# #endregion EXT:frontend:ReportModel\n\n# #region EXT:frontend:ReportsList [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportsList\n# #endregion EXT:frontend:ReportsList\n\n# #region EXT:frontend:RepositoryDashboardGrid [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: RepositoryDashboardGrid\n# #endregion EXT:frontend:RepositoryDashboardGrid\n\n# #region EXT:frontend:RoutePages [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: RoutePages\n# #endregion EXT:frontend:RoutePages\n\n# #region EXT:frontend:SessionRepositoryTests [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: SessionRepositoryTests\n# #endregion EXT:frontend:SessionRepositoryTests\n\n# #region EXT:frontend:SettingsUtils [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: SettingsUtils\n# #endregion EXT:frontend:SettingsUtils\n\n# #region EXT:frontend:SidebarNavigation [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: SidebarNavigation\n# #endregion EXT:frontend:SidebarNavigation\n\n# #region EXT:frontend:TaskModel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskModel\n# #endregion EXT:frontend:TaskModel\n\n# #region EXT:frontend:TaskPersistenceService.delete_tasks [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskPersistenceService.delete_tasks\n# #endregion EXT:frontend:TaskPersistenceService.delete_tasks\n\n# #region EXT:frontend:TaskPersistenceService.load_tasks [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskPersistenceService.load_tasks\n# #endregion EXT:frontend:TaskPersistenceService.load_tasks\n\n# #region EXT:frontend:TaskRunner [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskRunner\n# #endregion EXT:frontend:TaskRunner\n\n# #region EXT:frontend:TasksModule [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TasksModule\n# #endregion EXT:frontend:TasksModule\n\n# #region EXT:frontend:TestPolicyResolutionService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TestPolicyResolutionService\n# #endregion EXT:frontend:TestPolicyResolutionService\n\n# #region EXT:frontend:TestResourceService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TestResourceService\n# #endregion EXT:frontend:TestResourceService\n\n# #region EXT:frontend:TestScreenshotService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TestScreenshotService\n# #endregion EXT:frontend:TestScreenshotService\n\n# #region EXT:frontend:TopNavbar [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TopNavbar\n# #endregion EXT:frontend:TopNavbar\n\n# #region EXT:frontend:TypeProfiles [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TypeProfiles\n# #endregion EXT:frontend:TypeProfiles\n\n# #region EXT:frontend:activity [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: activity\n# #endregion EXT:frontend:activity\n\n# #region EXT:frontend:activityStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: activityStore\n# #endregion EXT:frontend:activityStore\n\n# #region EXT:frontend:addToast [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: addToast\n# #endregion EXT:frontend:addToast\n\n# #region EXT:frontend:api [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: api\n# #endregion EXT:frontend:api\n\n# #region EXT:frontend:api.client [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: api.client\n# #endregion EXT:frontend:api.client\n\n# #region EXT:frontend:api_module [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: api_module\n# #endregion EXT:frontend:api_module\n\n# #region EXT:frontend:assistantChat [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: assistantChat\n# #endregion EXT:frontend:assistantChat\n\n# #region EXT:frontend:assistantChatStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: assistantChatStore\n# #endregion EXT:frontend:assistantChatStore\n\n# #region EXT:frontend:authStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: authStore\n# #endregion EXT:frontend:authStore\n\n# #region EXT:frontend:checkTargetTableSchema [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: checkTargetTableSchema\n# #endregion EXT:frontend:checkTargetTableSchema\n\n# #region EXT:frontend:core_logger [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: core_logger\n# #endregion EXT:frontend:core_logger\n\n# #region EXT:frontend:datasetReviewSession [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: datasetReviewSession\n# #endregion EXT:frontend:datasetReviewSession\n\n# #region EXT:frontend:datasetReviewSessionStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: datasetReviewSessionStore\n# #endregion EXT:frontend:datasetReviewSessionStore\n\n# #region EXT:frontend:dictionaryApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: dictionaryApi\n# #endregion EXT:frontend:dictionaryApi\n\n# #region EXT:frontend:environmentContext [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: environmentContext\n# #endregion EXT:frontend:environmentContext\n\n# #region EXT:frontend:environmentContextStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: environmentContextStore\n# #endregion EXT:frontend:environmentContextStore\n\n# #region EXT:frontend:fetchApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: fetchApi\n# #endregion EXT:frontend:fetchApi\n\n# #region EXT:frontend:fetchDatabases [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: fetchDatabases\n# #endregion EXT:frontend:fetchDatabases\n\n# #region EXT:frontend:fetchEnvironments [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: fetchEnvironments\n# #endregion EXT:frontend:fetchEnvironments\n\n# #region EXT:frontend:gitService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: gitService\n# #endregion EXT:frontend:gitService\n\n# #region EXT:frontend:goto [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: goto\n# #endregion EXT:frontend:goto\n\n# #region EXT:frontend:handleUpdate [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: handleUpdate\n# #endregion EXT:frontend:handleUpdate\n\n# #region EXT:frontend:healthStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: healthStore\n# #endregion EXT:frontend:healthStore\n\n# #region EXT:frontend:i18n [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: i18n\n# #endregion EXT:frontend:i18n\n\n# #region EXT:frontend:i18n.profile.lookup_error [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: i18n.profile.lookup_error\n# #endregion EXT:frontend:i18n.profile.lookup_error\n\n# #region EXT:frontend:i18n.t [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: i18n.t\n# #endregion EXT:frontend:i18n.t\n\n# #region EXT:frontend:isProductionContextStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: isProductionContextStore\n# #endregion EXT:frontend:isProductionContextStore\n\n# #region EXT:frontend:migration.mappings.route [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: migration.mappings.route\n# #endregion EXT:frontend:migration.mappings.route\n\n# #region EXT:frontend:models.translate [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: models.translate\n# #endregion EXT:frontend:models.translate\n\n# #region EXT:frontend:normalize_report [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: normalize_report\n# #endregion EXT:frontend:normalize_report\n\n# #region EXT:frontend:orchestrator_lang_stats [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: orchestrator_lang_stats\n# #endregion EXT:frontend:orchestrator_lang_stats\n\n# #region EXT:frontend:policy_resolution_service [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: policy_resolution_service\n# #endregion EXT:frontend:policy_resolution_service\n\n# #region EXT:frontend:repository [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: repository\n# #endregion EXT:frontend:repository\n\n# #region EXT:frontend:requestApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: requestApi\n# #endregion EXT:frontend:requestApi\n\n# #region EXT:frontend:selectedEnvironmentStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: selectedEnvironmentStore\n# #endregion EXT:frontend:selectedEnvironmentStore\n\n# #region EXT:frontend:setupTests [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: setupTests\n# #endregion EXT:frontend:setupTests\n\n# #region EXT:frontend:sidebar [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: sidebar\n# #endregion EXT:frontend:sidebar\n\n# #region EXT:frontend:sidebarStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: sidebarStore\n# #endregion EXT:frontend:sidebarStore\n\n# #region EXT:frontend:stores [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: stores\n# #endregion EXT:frontend:stores\n\n# #region EXT:frontend:taskDrawer [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: taskDrawer\n# #endregion EXT:frontend:taskDrawer\n\n# #region EXT:frontend:taskDrawerStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: taskDrawerStore\n# #endregion EXT:frontend:taskDrawerStore\n\n# #region EXT:frontend:taskService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: taskService\n# #endregion EXT:frontend:taskService\n\n# #region EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: test_dashboard_validation_plugin_persists_task_and_environment_ids\n# #endregion EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids\n\n# #region EXT:frontend:test_llm_plugin_persistence [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: test_llm_plugin_persistence\n# #endregion EXT:frontend:test_llm_plugin_persistence\n\n# #region EXT:frontend:test_llm_provider [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: test_llm_provider\n# #endregion EXT:frontend:test_llm_provider\n\n# #region EXT:frontend:translationRunStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: translationRunStore\n# #endregion EXT:frontend:translationRunStore\n\n# #region EXT:internal:ASSISTANT_AUDIT [C:1] [TYPE External]\n# @BRIEF Internal module reference: ASSISTANT_AUDIT\n# #endregion EXT:internal:ASSISTANT_AUDIT\n\n# #region EXT:internal:All C4+ service and route modules [C:1] [TYPE External]\n# @BRIEF Internal module reference: All C4+ service and route modules\n# #endregion EXT:internal:All C4+ service and route modules\n\n# #region EXT:internal:All application modules [C:1] [TYPE External]\n# @BRIEF Internal module reference: All application modules\n# #endregion EXT:internal:All application modules\n\n# #region EXT:internal:CONVERSATIONS [C:1] [TYPE External]\n# @BRIEF Internal module reference: CONVERSATIONS\n# #endregion EXT:internal:CONVERSATIONS\n\n# #region EXT:internal:ConnectionContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: ConnectionContracts\n# #endregion EXT:internal:ConnectionContracts\n\n# #region EXT:internal:CoreContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: CoreContracts\n# #endregion EXT:internal:CoreContracts\n\n# #region EXT:internal:CotJsonFormat [C:1] [TYPE External]\n# @BRIEF Internal module reference: CotJsonFormat\n# #endregion EXT:internal:CotJsonFormat\n\n# #region EXT:internal:MappingBase [C:1] [TYPE External]\n# @BRIEF Internal module reference: MappingBase\n# #endregion EXT:internal:MappingBase\n\n# #region EXT:internal:MappingModels:Base [C:1] [TYPE External]\n# @BRIEF Internal module reference: MappingModels:Base\n# #endregion EXT:internal:MappingModels:Base\n\n# #region EXT:internal:NavigationContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: NavigationContracts\n# #endregion EXT:internal:NavigationContracts\n\n# #region EXT:internal:PageContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: PageContracts\n# #endregion EXT:internal:PageContracts\n\n# #region EXT:internal:Permissions [C:1] [TYPE External]\n# @BRIEF Internal module reference: Permissions\n# #endregion EXT:internal:Permissions\n\n# #region EXT:method:AsyncAPIClient._handle_http_error [C:1] [TYPE External]\n# @BRIEF Method reference: AsyncAPIClient._handle_http_error\n# #endregion EXT:method:AsyncAPIClient._handle_http_error\n\n# #region EXT:method:AsyncAPIClient.request [C:1] [TYPE External]\n# @BRIEF Method reference: AsyncAPIClient.request\n# #endregion EXT:method:AsyncAPIClient.request\n\n# #region EXT:method:BackupPlugin:execute [C:1] [TYPE External]\n# @BRIEF Method reference: BackupPlugin:execute\n# #endregion EXT:method:BackupPlugin:execute\n\n# #region EXT:method:GlobalSettings.app_timezone [C:1] [TYPE External]\n# @BRIEF Method reference: GlobalSettings.app_timezone\n# #endregion EXT:method:GlobalSettings.app_timezone\n\n# #region EXT:method:MappingService:get_suggestions [C:1] [TYPE External]\n# @BRIEF Method reference: MappingService:get_suggestions\n# #endregion EXT:method:MappingService:get_suggestions\n\n# #region EXT:method:MigrationPlugin:execute [C:1] [TYPE External]\n# @BRIEF Method reference: MigrationPlugin:execute\n# #endregion EXT:method:MigrationPlugin:execute\n\n# #region EXT:method:SessionEventLogger.log_event [C:1] [TYPE External]\n# @BRIEF Method reference: SessionEventLogger.log_event\n# #endregion EXT:method:SessionEventLogger.log_event\n\n# #region EXT:method:SupersetClient.compile_dataset_preview [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.compile_dataset_preview\n# #endregion EXT:method:SupersetClient.compile_dataset_preview\n\n# #region EXT:method:SupersetClient.get_dashboard [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dashboard\n# #endregion EXT:method:SupersetClient.get_dashboard\n\n# #region EXT:method:SupersetClient.get_dashboard_detail [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dashboard_detail\n# #endregion EXT:method:SupersetClient.get_dashboard_detail\n\n# #region EXT:method:SupersetClient.get_dashboards_summary [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dashboards_summary\n# #endregion EXT:method:SupersetClient.get_dashboards_summary\n\n# #region EXT:method:SupersetClient.get_dataset [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dataset\n# #endregion EXT:method:SupersetClient.get_dataset\n\n# #region EXT:method:SupersetClient.update_dataset [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.update_dataset\n# #endregion EXT:method:SupersetClient.update_dataset\n\n# #region EXT:method:SupersetContextExtractor.parse_superset_link [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetContextExtractor.parse_superset_link\n# #endregion EXT:method:SupersetContextExtractor.parse_superset_link\n\n# #region EXT:method:TASK_TYPE_PLUGIN_MAP [C:1] [TYPE External]\n# @BRIEF Method reference: TASK_TYPE_PLUGIN_MAP\n# #endregion EXT:method:TASK_TYPE_PLUGIN_MAP\n\n# #region EXT:method:TaskLogPersistenceService.add_logs [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.add_logs\n# #endregion EXT:method:TaskLogPersistenceService.add_logs\n\n# #region EXT:method:TaskLogPersistenceService.delete_logs_for_tasks [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.delete_logs_for_tasks\n# #endregion EXT:method:TaskLogPersistenceService.delete_logs_for_tasks\n\n# #region EXT:method:TaskLogPersistenceService.get_log_stats [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.get_log_stats\n# #endregion EXT:method:TaskLogPersistenceService.get_log_stats\n\n# #region EXT:method:TaskLogPersistenceService.get_logs [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.get_logs\n# #endregion EXT:method:TaskLogPersistenceService.get_logs\n\n# #region EXT:method:TaskLogPersistenceService.get_sources [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.get_sources\n# #endregion EXT:method:TaskLogPersistenceService.get_sources\n\n# #region EXT:method:TaskManager.create_task [C:1] [TYPE External]\n# @BRIEF Method reference: TaskManager.create_task\n# #endregion EXT:method:TaskManager.create_task\n\n# #region EXT:method:TranslationExecutor._auto_size_batches [C:1] [TYPE External]\n# @BRIEF Method reference: TranslationExecutor._auto_size_batches\n# #endregion EXT:method:TranslationExecutor._auto_size_batches\n\n# #region EXT:method:_build_body [C:1] [TYPE External]\n# @BRIEF Method reference: _build_body\n# #endregion EXT:method:_build_body\n\n# #region EXT:method:_extract_resource_name_from_task [C:1] [TYPE External]\n# @BRIEF Method reference: _extract_resource_name_from_task\n# #endregion EXT:method:_extract_resource_name_from_task\n\n# #region EXT:method:_extract_resource_type_from_task [C:1] [TYPE External]\n# @BRIEF Method reference: _extract_resource_type_from_task\n# #endregion EXT:method:_extract_resource_type_from_task\n\n# #region EXT:method:_find_dashboard_owners [C:1] [TYPE External]\n# @BRIEF Method reference: _find_dashboard_owners\n# #endregion EXT:method:_find_dashboard_owners\n\n# #region EXT:method:_get_git_status_for_dashboard [C:1] [TYPE External]\n# @BRIEF Method reference: _get_git_status_for_dashboard\n# #endregion EXT:method:_get_git_status_for_dashboard\n\n# #region EXT:method:_get_last_llm_task_for_dashboard [C:1] [TYPE External]\n# @BRIEF Method reference: _get_last_llm_task_for_dashboard\n# #endregion EXT:method:_get_last_llm_task_for_dashboard\n\n# #region EXT:method:_get_last_task_for_resource [C:1] [TYPE External]\n# @BRIEF Method reference: _get_last_task_for_resource\n# #endregion EXT:method:_get_last_task_for_resource\n\n# #region EXT:method:_initialize_providers [C:1] [TYPE External]\n# @BRIEF Method reference: _initialize_providers\n# #endregion EXT:method:_initialize_providers\n\n# #region EXT:method:_llm_http [C:1] [TYPE External]\n# @BRIEF Method reference: _llm_http\n# #endregion EXT:method:_llm_http\n\n# #region EXT:method:_normalize_datetime_for_compare [C:1] [TYPE External]\n# @BRIEF Method reference: _normalize_datetime_for_compare\n# #endregion EXT:method:_normalize_datetime_for_compare\n\n# #region EXT:method:_normalize_task_status [C:1] [TYPE External]\n# @BRIEF Method reference: _normalize_task_status\n# #endregion EXT:method:_normalize_task_status\n\n# #region EXT:method:_normalize_validation_status [C:1] [TYPE External]\n# @BRIEF Method reference: _normalize_validation_status\n# #endregion EXT:method:_normalize_validation_status\n\n# #region EXT:method:_prime_dashboard_meta_cache [C:1] [TYPE External]\n# @BRIEF Method reference: _prime_dashboard_meta_cache\n# #endregion EXT:method:_prime_dashboard_meta_cache\n\n# #region EXT:method:_resolve_dashboard_meta [C:1] [TYPE External]\n# @BRIEF Method reference: _resolve_dashboard_meta\n# #endregion EXT:method:_resolve_dashboard_meta\n\n# #region EXT:method:_resolve_targets [C:1] [TYPE External]\n# @BRIEF Method reference: _resolve_targets\n# #endregion EXT:method:_resolve_targets\n\n# #region EXT:method:_should_notify [C:1] [TYPE External]\n# @BRIEF Method reference: _should_notify\n# #endregion EXT:method:_should_notify\n\n# #region EXT:method:decrypt [C:1] [TYPE External]\n# @BRIEF Method reference: decrypt\n# #endregion EXT:method:decrypt\n\n# #region EXT:method:encrypt [C:1] [TYPE External]\n# @BRIEF Method reference: encrypt\n# #endregion EXT:method:encrypt\n\n# #region EXT:method:get_activity_summary [C:1] [TYPE External]\n# @BRIEF Method reference: get_activity_summary\n# #endregion EXT:method:get_activity_summary\n\n# #region EXT:method:get_dashboards_with_status [C:1] [TYPE External]\n# @BRIEF Method reference: get_dashboards_with_status\n# #endregion EXT:method:get_dashboards_with_status\n\n# #region EXT:method:get_datasets_with_status [C:1] [TYPE External]\n# @BRIEF Method reference: get_datasets_with_status\n# #endregion EXT:method:get_datasets_with_status\n\n# #region EXT:method:get_repo [C:1] [TYPE External]\n# @BRIEF Method reference: get_repo\n# #endregion EXT:method:get_repo\n\n# #region EXT:method:schemas.translate.TargetSchemaValidationRequest [C:1] [TYPE External]\n# @BRIEF Method reference: schemas.translate.TargetSchemaValidationRequest\n# #endregion EXT:method:schemas.translate.TargetSchemaValidationRequest\n\n# #region EXT:method:schemas.translate.TargetSchemaValidationResponse [C:1] [TYPE External]\n# @BRIEF Method reference: schemas.translate.TargetSchemaValidationResponse\n# #endregion EXT:method:schemas.translate.TargetSchemaValidationResponse\n\n# #region EXT:method:self.network.request [C:1] [TYPE External]\n# @BRIEF Method reference: self.network.request\n# #endregion EXT:method:self.network.request\n\n# #region EXT:module:TargetName [C:1] [TYPE External]\n# @BRIEF External reference: TargetName\n# #endregion EXT:module:TargetName\n\n# #region EXT:path:EnvSelector.svelte [C:1] [TYPE External]\n# @BRIEF File path: EnvSelector.svelte\n# #endregion EXT:path:EnvSelector.svelte\n\n# #region EXT:path:MappingTable.svelte [C:1] [TYPE External]\n# @BRIEF File path: MappingTable.svelte\n# #endregion EXT:path:MappingTable.svelte\n\n# #region EXT:path:backend/requirements.txt [C:1] [TYPE External]\n# @BRIEF File path: backend/requirements.txt\n# #endregion EXT:path:backend/requirements.txt\n\n# #region EXT:path:docker/backend.entrypoint.sh [C:1] [TYPE External]\n# @BRIEF File path: docker/backend.entrypoint.sh\n# #endregion EXT:path:docker/backend.entrypoint.sh\n\n# #region EXT:path:docker/frontend.entrypoint.sh [C:1] [TYPE External]\n# @BRIEF File path: docker/frontend.entrypoint.sh\n# #endregion EXT:path:docker/frontend.entrypoint.sh\n\n# #region EXT:path:docker/nginx.conf [C:1] [TYPE External]\n# @BRIEF File path: docker/nginx.conf\n# #endregion EXT:path:docker/nginx.conf\n\n# #region EXT:path:docker/nginx.ssl.conf [C:1] [TYPE External]\n# @BRIEF File path: docker/nginx.ssl.conf\n# #endregion EXT:path:docker/nginx.ssl.conf\n\n# #region EXT:path:frontend/package.json [C:1] [TYPE External]\n# @BRIEF File path: frontend/package.json\n# #endregion EXT:path:frontend/package.json\n\n# #region EXT:path:frontend/src/components/EnvSelector.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/EnvSelector.svelte\n# #endregion EXT:path:frontend/src/components/EnvSelector.svelte\n\n# #region EXT:path:frontend/src/components/MappingTable.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/MappingTable.svelte\n# #endregion EXT:path:frontend/src/components/MappingTable.svelte\n\n# #region EXT:path:frontend/src/components/PasswordPrompt.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/PasswordPrompt.svelte\n# #endregion EXT:path:frontend/src/components/PasswordPrompt.svelte\n\n# #region EXT:path:frontend/src/components/TaskHistory.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/TaskHistory.svelte\n# #endregion EXT:path:frontend/src/components/TaskHistory.svelte\n\n# #region EXT:path:frontend/src/lib/api.js [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/lib/api.js\n# #endregion EXT:path:frontend/src/lib/api.js\n\n# #region EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES [C:1] [TYPE External]\n# @BRIEF External reference: SUPPORTED_DENSITIES\n# #endregion EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES\n\n# #region EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES [C:1] [TYPE External]\n# @BRIEF External reference: SUPPORTED_START_PAGES\n# #endregion EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES\n\n# #region EXT:requests [C:1] [TYPE External]\n# @BRIEF External reference: requests\n# #endregion EXT:requests\n\n# #region EXT:spec:BulkReplaceModal:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: BulkReplaceModal:Component\n# #endregion EXT:spec:BulkReplaceModal:Component\n\n# #region EXT:spec:CorrectionCell:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: CorrectionCell:Component\n# #endregion EXT:spec:CorrectionCell:Component\n\n# #region EXT:spec:StatsBar:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: StatsBar:Component\n# #endregion EXT:spec:StatsBar:Component\n\n# #region EXT:spec:TargetSchemaHint:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: TargetSchemaHint:Component\n# #endregion EXT:spec:TargetSchemaHint:Component\n\n# #region EXT:spec:TranslationPreview:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: TranslationPreview:Component\n# #endregion EXT:spec:TranslationPreview:Component\n\n# #region EXT:ss-tools:log_security_event [C:1] [TYPE External]\n# @BRIEF External reference: log_security_event\n# #endregion EXT:ss-tools:log_security_event\n\n# #region EXT:sveltekit:$app/environment [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $app/environment\n# #endregion EXT:sveltekit:$app/environment\n\n# #region EXT:sveltekit:$app/navigation [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $app/navigation\n# #endregion EXT:sveltekit:$app/navigation\n\n# #region EXT:sveltekit:$app/stores [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $app/stores\n# #endregion EXT:sveltekit:$app/stores\n\n# #region EXT:sveltekit:$env/static/public [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $env/static/public\n# #endregion EXT:sveltekit:$env/static/public\n\n# #endregion ExternalStubs\n" - }, - { - "contract_id": "EXT:FastAPI:TestClient", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 10, - "end_line": 12, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: TestClient", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:FastAPI:TestClient [C:1] [TYPE External]\n# @BRIEF External reference: TestClient\n# #endregion EXT:FastAPI:TestClient\n" - }, - { - "contract_id": "EXT:Library:SQLAlchemy.Base", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 14, - "end_line": 16, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: SQLAlchemy.Base", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:SQLAlchemy.Base [C:1] [TYPE External]\n# @BRIEF External library: SQLAlchemy.Base\n# #endregion EXT:Library:SQLAlchemy.Base\n" - }, - { - "contract_id": "EXT:Library:authlib", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 18, - "end_line": 20, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: authlib", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:authlib [C:1] [TYPE External]\n# @BRIEF External library: authlib\n# #endregion EXT:Library:authlib\n" - }, - { - "contract_id": "EXT:Library:fastapi.APIRouter", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 22, - "end_line": 24, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: fastapi.APIRouter", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:fastapi.APIRouter [C:1] [TYPE External]\n# @BRIEF External library: fastapi.APIRouter\n# #endregion EXT:Library:fastapi.APIRouter\n" - }, - { - "contract_id": "EXT:Library:pydantic", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 26, - "end_line": 28, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: pydantic", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:pydantic [C:1] [TYPE External]\n# @BRIEF External library: pydantic\n# #endregion EXT:Library:pydantic\n" - }, - { - "contract_id": "EXT:Library:pytest", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 30, - "end_line": 32, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: pytest", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:pytest [C:1] [TYPE External]\n# @BRIEF External library: pytest\n# #endregion EXT:Library:pytest\n" - }, - { - "contract_id": "EXT:Library:sqlalchemy", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 34, - "end_line": 36, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: sqlalchemy", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:sqlalchemy [C:1] [TYPE External]\n# @BRIEF External library: sqlalchemy\n# #endregion EXT:Library:sqlalchemy\n" - }, - { - "contract_id": "EXT:Library:sqlalchemy.orm.Session", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 38, - "end_line": 40, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: sqlalchemy.orm.Session", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:sqlalchemy.orm.Session [C:1] [TYPE External]\n# @BRIEF External library: sqlalchemy.orm.Session\n# #endregion EXT:Library:sqlalchemy.orm.Session\n" - }, - { - "contract_id": "EXT:Library:yaml", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 42, - "end_line": 44, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: yaml", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:yaml [C:1] [TYPE External]\n# @BRIEF External library: yaml\n# #endregion EXT:Library:yaml\n" - }, - { - "contract_id": "EXT:Python:Exception", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 46, - "end_line": 48, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: Exception", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:Exception [C:1] [TYPE External]\n# @BRIEF Python standard library module: Exception\n# #endregion EXT:Python:Exception\n" - }, - { - "contract_id": "EXT:Python:hashlib", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 50, - "end_line": 52, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: hashlib", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:hashlib [C:1] [TYPE External]\n# @BRIEF Python standard library module: hashlib\n# #endregion EXT:Python:hashlib\n" - }, - { - "contract_id": "EXT:Python:hashlib.sha256", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 54, - "end_line": 56, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: hashlib.sha256", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:hashlib.sha256 [C:1] [TYPE External]\n# @BRIEF Python standard library module: hashlib.sha256\n# #endregion EXT:Python:hashlib.sha256\n" - }, - { - "contract_id": "EXT:Python:json", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 58, - "end_line": 60, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: json", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:json [C:1] [TYPE External]\n# @BRIEF Python standard library module: json\n# #endregion EXT:Python:json\n" - }, - { - "contract_id": "EXT:Python:secrets", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 62, - "end_line": 64, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: secrets", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:secrets [C:1] [TYPE External]\n# @BRIEF Python standard library module: secrets\n# #endregion EXT:Python:secrets\n" - }, - { - "contract_id": "EXT:Python:secrets.token_urlsafe", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 66, - "end_line": 68, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: secrets.token_urlsafe", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:secrets.token_urlsafe [C:1] [TYPE External]\n# @BRIEF Python standard library module: secrets.token_urlsafe\n# #endregion EXT:Python:secrets.token_urlsafe\n" - }, - { - "contract_id": "EXT:Python:uuid", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 70, - "end_line": 72, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library module: uuid", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:uuid [C:1] [TYPE External]\n# @BRIEF Python standard library module: uuid\n# #endregion EXT:Python:uuid\n" - }, - { - "contract_id": "EXT:build.sh", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 74, - "end_line": 76, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: build.sh", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:build.sh [C:1] [TYPE External]\n# @BRIEF External reference: build.sh\n# #endregion EXT:build.sh\n" - }, - { - "contract_id": "EXT:frontend:AssistantChatPanel", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 90, - "end_line": 92, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: AssistantChatPanel", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:AssistantChatPanel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: AssistantChatPanel\n# #endregion EXT:frontend:AssistantChatPanel\n" - }, - { - "contract_id": "EXT:frontend:AssistantFirstMessageIntegrationTest", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 94, - "end_line": 96, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: AssistantFirstMessageIntegrationTest", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:AssistantFirstMessageIntegrationTest [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: AssistantFirstMessageIntegrationTest\n# #endregion EXT:frontend:AssistantFirstMessageIntegrationTest\n" - }, - { - "contract_id": "EXT:frontend:BranchSelector", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 98, - "end_line": 100, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: BranchSelector", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:BranchSelector [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: BranchSelector\n# #endregion EXT:frontend:BranchSelector\n" - }, - { - "contract_id": "EXT:frontend:Breadcrumbs", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 102, - "end_line": 104, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: Breadcrumbs", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:Breadcrumbs [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: Breadcrumbs\n# #endregion EXT:frontend:Breadcrumbs\n" - }, - { - "contract_id": "EXT:frontend:ConflictResolver", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 106, - "end_line": 108, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ConflictResolver", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ConflictResolver [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ConflictResolver\n# #endregion EXT:frontend:ConflictResolver\n" - }, - { - "contract_id": "EXT:frontend:DashboardHub", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 110, - "end_line": 112, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: DashboardHub", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DashboardHub [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: DashboardHub\n# #endregion EXT:frontend:DashboardHub\n" - }, - { - "contract_id": "EXT:frontend:DashboardValidationService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 114, - "end_line": 116, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: DashboardValidationService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DashboardValidationService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: DashboardValidationService\n# #endregion EXT:frontend:DashboardValidationService\n" - }, - { - "contract_id": "EXT:frontend:DeploymentModal", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 118, - "end_line": 120, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: DeploymentModal", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DeploymentModal [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: DeploymentModal\n# #endregion EXT:frontend:DeploymentModal\n" - }, - { - "contract_id": "EXT:frontend:EnvironmentsTab", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 122, - "end_line": 124, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: EnvironmentsTab", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:EnvironmentsTab [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: EnvironmentsTab\n# #endregion EXT:frontend:EnvironmentsTab\n" - }, - { - "contract_id": "EXT:frontend:GitApi", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 126, - "end_line": 128, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: GitApi", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:GitApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: GitApi\n# #endregion EXT:frontend:GitApi\n" - }, - { - "contract_id": "EXT:frontend:GitSettingsPage", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 130, - "end_line": 132, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: GitSettingsPage", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:GitSettingsPage [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: GitSettingsPage\n# #endregion EXT:frontend:GitSettingsPage\n" - }, - { - "contract_id": "EXT:frontend:LocaleEn", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 134, - "end_line": 136, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: LocaleEn", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:LocaleEn [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: LocaleEn\n# #endregion EXT:frontend:LocaleEn\n" - }, - { - "contract_id": "EXT:frontend:LocaleRu", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 138, - "end_line": 140, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: LocaleRu", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:LocaleRu [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: LocaleRu\n# #endregion EXT:frontend:LocaleRu\n" - }, - { - "contract_id": "EXT:frontend:MaintenanceEventsTable", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 142, - "end_line": 144, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: MaintenanceEventsTable", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:MaintenanceEventsTable [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: MaintenanceEventsTable\n# #endregion EXT:frontend:MaintenanceEventsTable\n" - }, - { - "contract_id": "EXT:frontend:MaintenanceServiceModule", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 146, - "end_line": 148, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: MaintenanceServiceModule", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:MaintenanceServiceModule [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: MaintenanceServiceModule\n# #endregion EXT:frontend:MaintenanceServiceModule\n" - }, - { - "contract_id": "EXT:frontend:MaintenanceSettingsPanel", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 150, - "end_line": 152, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: MaintenanceSettingsPanel", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:MaintenanceSettingsPanel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: MaintenanceSettingsPanel\n# #endregion EXT:frontend:MaintenanceSettingsPanel\n" - }, - { - "contract_id": "EXT:frontend:PluginLoaderCore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 154, - "end_line": 156, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: PluginLoaderCore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:PluginLoaderCore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: PluginLoaderCore\n# #endregion EXT:frontend:PluginLoaderCore\n" - }, - { - "contract_id": "EXT:frontend:ProfilePageBindAccountFlowTests", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 158, - "end_line": 160, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ProfilePageBindAccountFlowTests", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ProfilePageBindAccountFlowTests [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ProfilePageBindAccountFlowTests\n# #endregion EXT:frontend:ProfilePageBindAccountFlowTests\n" - }, - { - "contract_id": "EXT:frontend:ProtectedRoute", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 162, - "end_line": 164, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ProtectedRoute", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ProtectedRoute [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ProtectedRoute\n# #endregion EXT:frontend:ProtectedRoute\n" - }, - { - "contract_id": "EXT:frontend:ProviderConfigIntegrationTest", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 166, - "end_line": 168, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ProviderConfigIntegrationTest", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ProviderConfigIntegrationTest [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ProviderConfigIntegrationTest\n# #endregion EXT:frontend:ProviderConfigIntegrationTest\n" - }, - { - "contract_id": "EXT:frontend:ReportCard", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 170, - "end_line": 172, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ReportCard", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ReportCard [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportCard\n# #endregion EXT:frontend:ReportCard\n" - }, - { - "contract_id": "EXT:frontend:ReportDetailPanel", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 174, - "end_line": 176, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ReportDetailPanel", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ReportDetailPanel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportDetailPanel\n# #endregion EXT:frontend:ReportDetailPanel\n" - }, - { - "contract_id": "EXT:frontend:ReportModel", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 178, - "end_line": 180, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ReportModel", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ReportModel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportModel\n# #endregion EXT:frontend:ReportModel\n" - }, - { - "contract_id": "EXT:frontend:ReportsList", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 182, - "end_line": 184, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: ReportsList", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ReportsList [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: ReportsList\n# #endregion EXT:frontend:ReportsList\n" - }, - { - "contract_id": "EXT:frontend:RepositoryDashboardGrid", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 186, - "end_line": 188, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: RepositoryDashboardGrid", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:RepositoryDashboardGrid [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: RepositoryDashboardGrid\n# #endregion EXT:frontend:RepositoryDashboardGrid\n" - }, - { - "contract_id": "EXT:frontend:RoutePages", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 190, - "end_line": 192, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: RoutePages", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:RoutePages [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: RoutePages\n# #endregion EXT:frontend:RoutePages\n" - }, - { - "contract_id": "EXT:frontend:SessionRepositoryTests", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 194, - "end_line": 196, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: SessionRepositoryTests", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:SessionRepositoryTests [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: SessionRepositoryTests\n# #endregion EXT:frontend:SessionRepositoryTests\n" - }, - { - "contract_id": "EXT:frontend:SettingsUtils", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 198, - "end_line": 200, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: SettingsUtils", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:SettingsUtils [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: SettingsUtils\n# #endregion EXT:frontend:SettingsUtils\n" - }, - { - "contract_id": "EXT:frontend:SidebarNavigation", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 202, - "end_line": 204, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: SidebarNavigation", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:SidebarNavigation [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: SidebarNavigation\n# #endregion EXT:frontend:SidebarNavigation\n" - }, - { - "contract_id": "EXT:frontend:TaskModel", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 206, - "end_line": 208, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TaskModel", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TaskModel [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskModel\n# #endregion EXT:frontend:TaskModel\n" - }, - { - "contract_id": "EXT:frontend:TaskPersistenceService.delete_tasks", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 210, - "end_line": 212, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TaskPersistenceService.delete_tasks", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TaskPersistenceService.delete_tasks [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskPersistenceService.delete_tasks\n# #endregion EXT:frontend:TaskPersistenceService.delete_tasks\n" - }, - { - "contract_id": "EXT:frontend:TaskPersistenceService.load_tasks", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 214, - "end_line": 216, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TaskPersistenceService.load_tasks", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TaskPersistenceService.load_tasks [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskPersistenceService.load_tasks\n# #endregion EXT:frontend:TaskPersistenceService.load_tasks\n" - }, - { - "contract_id": "EXT:frontend:TaskRunner", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 218, - "end_line": 220, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TaskRunner", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TaskRunner [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TaskRunner\n# #endregion EXT:frontend:TaskRunner\n" - }, - { - "contract_id": "EXT:frontend:TasksModule", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 222, - "end_line": 224, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TasksModule", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TasksModule [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TasksModule\n# #endregion EXT:frontend:TasksModule\n" - }, - { - "contract_id": "EXT:frontend:TestPolicyResolutionService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 226, - "end_line": 228, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TestPolicyResolutionService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TestPolicyResolutionService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TestPolicyResolutionService\n# #endregion EXT:frontend:TestPolicyResolutionService\n" - }, - { - "contract_id": "EXT:frontend:TestResourceService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 230, - "end_line": 232, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TestResourceService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TestResourceService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TestResourceService\n# #endregion EXT:frontend:TestResourceService\n" - }, - { - "contract_id": "EXT:frontend:TestScreenshotService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 234, - "end_line": 236, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TestScreenshotService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TestScreenshotService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TestScreenshotService\n# #endregion EXT:frontend:TestScreenshotService\n" - }, - { - "contract_id": "EXT:frontend:TopNavbar", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 238, - "end_line": 240, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TopNavbar", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TopNavbar [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TopNavbar\n# #endregion EXT:frontend:TopNavbar\n" - }, - { - "contract_id": "EXT:frontend:TypeProfiles", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 242, - "end_line": 244, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: TypeProfiles", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TypeProfiles [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: TypeProfiles\n# #endregion EXT:frontend:TypeProfiles\n" - }, - { - "contract_id": "EXT:frontend:activity", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 246, - "end_line": 248, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: activity", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:activity [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: activity\n# #endregion EXT:frontend:activity\n" - }, - { - "contract_id": "EXT:frontend:activityStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 250, - "end_line": 252, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: activityStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:activityStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: activityStore\n# #endregion EXT:frontend:activityStore\n" - }, - { - "contract_id": "EXT:frontend:addToast", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 254, - "end_line": 256, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: addToast", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:addToast [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: addToast\n# #endregion EXT:frontend:addToast\n" - }, - { - "contract_id": "EXT:frontend:api", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 258, - "end_line": 260, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: api", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:api [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: api\n# #endregion EXT:frontend:api\n" - }, - { - "contract_id": "EXT:frontend:api.client", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 262, - "end_line": 264, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: api.client", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:api.client [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: api.client\n# #endregion EXT:frontend:api.client\n" - }, - { - "contract_id": "EXT:frontend:api_module", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 266, - "end_line": 268, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: api_module", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:api_module [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: api_module\n# #endregion EXT:frontend:api_module\n" - }, - { - "contract_id": "EXT:frontend:assistantChat", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 270, - "end_line": 272, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: assistantChat", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:assistantChat [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: assistantChat\n# #endregion EXT:frontend:assistantChat\n" - }, - { - "contract_id": "EXT:frontend:assistantChatStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 274, - "end_line": 276, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: assistantChatStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:assistantChatStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: assistantChatStore\n# #endregion EXT:frontend:assistantChatStore\n" - }, - { - "contract_id": "EXT:frontend:authStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 278, - "end_line": 280, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: authStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:authStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: authStore\n# #endregion EXT:frontend:authStore\n" - }, - { - "contract_id": "EXT:frontend:checkTargetTableSchema", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 282, - "end_line": 284, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: checkTargetTableSchema", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:checkTargetTableSchema [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: checkTargetTableSchema\n# #endregion EXT:frontend:checkTargetTableSchema\n" - }, - { - "contract_id": "EXT:frontend:core_logger", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 286, - "end_line": 288, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: core_logger", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:core_logger [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: core_logger\n# #endregion EXT:frontend:core_logger\n" - }, - { - "contract_id": "EXT:frontend:datasetReviewSession", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 290, - "end_line": 292, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: datasetReviewSession", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:datasetReviewSession [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: datasetReviewSession\n# #endregion EXT:frontend:datasetReviewSession\n" - }, - { - "contract_id": "EXT:frontend:datasetReviewSessionStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 294, - "end_line": 296, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: datasetReviewSessionStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:datasetReviewSessionStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: datasetReviewSessionStore\n# #endregion EXT:frontend:datasetReviewSessionStore\n" - }, - { - "contract_id": "EXT:frontend:dictionaryApi", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 298, - "end_line": 300, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: dictionaryApi", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:dictionaryApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: dictionaryApi\n# #endregion EXT:frontend:dictionaryApi\n" - }, - { - "contract_id": "EXT:frontend:environmentContext", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 302, - "end_line": 304, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: environmentContext", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:environmentContext [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: environmentContext\n# #endregion EXT:frontend:environmentContext\n" - }, - { - "contract_id": "EXT:frontend:environmentContextStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 306, - "end_line": 308, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: environmentContextStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:environmentContextStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: environmentContextStore\n# #endregion EXT:frontend:environmentContextStore\n" - }, - { - "contract_id": "EXT:frontend:fetchApi", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 310, - "end_line": 312, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: fetchApi", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:fetchApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: fetchApi\n# #endregion EXT:frontend:fetchApi\n" - }, - { - "contract_id": "EXT:frontend:fetchDatabases", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 314, - "end_line": 316, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: fetchDatabases", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:fetchDatabases [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: fetchDatabases\n# #endregion EXT:frontend:fetchDatabases\n" - }, - { - "contract_id": "EXT:frontend:fetchEnvironments", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 318, - "end_line": 320, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: fetchEnvironments", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:fetchEnvironments [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: fetchEnvironments\n# #endregion EXT:frontend:fetchEnvironments\n" - }, - { - "contract_id": "EXT:frontend:gitService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 322, - "end_line": 324, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: gitService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:gitService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: gitService\n# #endregion EXT:frontend:gitService\n" - }, - { - "contract_id": "EXT:frontend:goto", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 326, - "end_line": 328, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: goto", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:goto [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: goto\n# #endregion EXT:frontend:goto\n" - }, - { - "contract_id": "EXT:frontend:handleUpdate", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 330, - "end_line": 332, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: handleUpdate", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:handleUpdate [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: handleUpdate\n# #endregion EXT:frontend:handleUpdate\n" - }, - { - "contract_id": "EXT:frontend:healthStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 334, - "end_line": 336, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: healthStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:healthStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: healthStore\n# #endregion EXT:frontend:healthStore\n" - }, - { - "contract_id": "EXT:frontend:i18n", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 338, - "end_line": 340, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: i18n", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:i18n [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: i18n\n# #endregion EXT:frontend:i18n\n" - }, - { - "contract_id": "EXT:frontend:i18n.profile.lookup_error", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 342, - "end_line": 344, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: i18n.profile.lookup_error", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:i18n.profile.lookup_error [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: i18n.profile.lookup_error\n# #endregion EXT:frontend:i18n.profile.lookup_error\n" - }, - { - "contract_id": "EXT:frontend:i18n.t", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 346, - "end_line": 348, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: i18n.t", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:i18n.t [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: i18n.t\n# #endregion EXT:frontend:i18n.t\n" - }, - { - "contract_id": "EXT:frontend:isProductionContextStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 350, - "end_line": 352, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: isProductionContextStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:isProductionContextStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: isProductionContextStore\n# #endregion EXT:frontend:isProductionContextStore\n" - }, - { - "contract_id": "EXT:frontend:migration.mappings.route", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 354, - "end_line": 356, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: migration.mappings.route", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:migration.mappings.route [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: migration.mappings.route\n# #endregion EXT:frontend:migration.mappings.route\n" - }, - { - "contract_id": "EXT:frontend:models.translate", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 358, - "end_line": 360, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: models.translate", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:models.translate [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: models.translate\n# #endregion EXT:frontend:models.translate\n" - }, - { - "contract_id": "EXT:frontend:normalize_report", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 362, - "end_line": 364, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: normalize_report", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:normalize_report [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: normalize_report\n# #endregion EXT:frontend:normalize_report\n" - }, - { - "contract_id": "EXT:frontend:orchestrator_lang_stats", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 366, - "end_line": 368, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: orchestrator_lang_stats", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:orchestrator_lang_stats [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: orchestrator_lang_stats\n# #endregion EXT:frontend:orchestrator_lang_stats\n" - }, - { - "contract_id": "EXT:frontend:policy_resolution_service", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 370, - "end_line": 372, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: policy_resolution_service", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:policy_resolution_service [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: policy_resolution_service\n# #endregion EXT:frontend:policy_resolution_service\n" - }, - { - "contract_id": "EXT:frontend:repository", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 374, - "end_line": 376, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: repository", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:repository [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: repository\n# #endregion EXT:frontend:repository\n" - }, - { - "contract_id": "EXT:frontend:requestApi", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 378, - "end_line": 380, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: requestApi", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:requestApi [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: requestApi\n# #endregion EXT:frontend:requestApi\n" - }, - { - "contract_id": "EXT:frontend:selectedEnvironmentStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 382, - "end_line": 384, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: selectedEnvironmentStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:selectedEnvironmentStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: selectedEnvironmentStore\n# #endregion EXT:frontend:selectedEnvironmentStore\n" - }, - { - "contract_id": "EXT:frontend:setupTests", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 386, - "end_line": 388, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: setupTests", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:setupTests [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: setupTests\n# #endregion EXT:frontend:setupTests\n" - }, - { - "contract_id": "EXT:frontend:sidebar", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 390, - "end_line": 392, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: sidebar", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:sidebar [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: sidebar\n# #endregion EXT:frontend:sidebar\n" - }, - { - "contract_id": "EXT:frontend:sidebarStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 394, - "end_line": 396, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: sidebarStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:sidebarStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: sidebarStore\n# #endregion EXT:frontend:sidebarStore\n" - }, - { - "contract_id": "EXT:frontend:stores", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 398, - "end_line": 400, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: stores", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:stores [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: stores\n# #endregion EXT:frontend:stores\n" - }, - { - "contract_id": "EXT:frontend:taskDrawer", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 402, - "end_line": 404, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: taskDrawer", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:taskDrawer [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: taskDrawer\n# #endregion EXT:frontend:taskDrawer\n" - }, - { - "contract_id": "EXT:frontend:taskDrawerStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 406, - "end_line": 408, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: taskDrawerStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:taskDrawerStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: taskDrawerStore\n# #endregion EXT:frontend:taskDrawerStore\n" - }, - { - "contract_id": "EXT:frontend:taskService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 410, - "end_line": 412, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: taskService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:taskService [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: taskService\n# #endregion EXT:frontend:taskService\n" - }, - { - "contract_id": "EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 414, - "end_line": 416, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: test_dashboard_validation_plugin_persists_task_and_environment_ids", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: test_dashboard_validation_plugin_persists_task_and_environment_ids\n# #endregion EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids\n" - }, - { - "contract_id": "EXT:frontend:test_llm_plugin_persistence", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 418, - "end_line": 420, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: test_llm_plugin_persistence", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:test_llm_plugin_persistence [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: test_llm_plugin_persistence\n# #endregion EXT:frontend:test_llm_plugin_persistence\n" - }, - { - "contract_id": "EXT:frontend:test_llm_provider", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 422, - "end_line": 424, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: test_llm_provider", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:test_llm_provider [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: test_llm_provider\n# #endregion EXT:frontend:test_llm_provider\n" - }, - { - "contract_id": "EXT:frontend:translationRunStore", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 426, - "end_line": 428, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store/service/component: translationRunStore", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:translationRunStore [C:1] [TYPE External]\n# @BRIEF Frontend store/service/component: translationRunStore\n# #endregion EXT:frontend:translationRunStore\n" - }, - { - "contract_id": "EXT:internal:ASSISTANT_AUDIT", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 430, - "end_line": 432, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: ASSISTANT_AUDIT", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:ASSISTANT_AUDIT [C:1] [TYPE External]\n# @BRIEF Internal module reference: ASSISTANT_AUDIT\n# #endregion EXT:internal:ASSISTANT_AUDIT\n" - }, - { - "contract_id": "EXT:internal:CONVERSATIONS", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 442, - "end_line": 444, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: CONVERSATIONS", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:CONVERSATIONS [C:1] [TYPE External]\n# @BRIEF Internal module reference: CONVERSATIONS\n# #endregion EXT:internal:CONVERSATIONS\n" - }, - { - "contract_id": "EXT:internal:ConnectionContracts", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 446, - "end_line": 448, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: ConnectionContracts", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:ConnectionContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: ConnectionContracts\n# #endregion EXT:internal:ConnectionContracts\n" - }, - { - "contract_id": "EXT:internal:CoreContracts", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 450, - "end_line": 452, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: CoreContracts", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:CoreContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: CoreContracts\n# #endregion EXT:internal:CoreContracts\n" - }, - { - "contract_id": "EXT:internal:CotJsonFormat", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 454, - "end_line": 456, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: CotJsonFormat", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:CotJsonFormat [C:1] [TYPE External]\n# @BRIEF Internal module reference: CotJsonFormat\n# #endregion EXT:internal:CotJsonFormat\n" - }, - { - "contract_id": "EXT:internal:MappingBase", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 458, - "end_line": 460, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: MappingBase", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:MappingBase [C:1] [TYPE External]\n# @BRIEF Internal module reference: MappingBase\n# #endregion EXT:internal:MappingBase\n" - }, - { - "contract_id": "EXT:internal:MappingModels:Base", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 462, - "end_line": 464, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: MappingModels:Base", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:MappingModels:Base [C:1] [TYPE External]\n# @BRIEF Internal module reference: MappingModels:Base\n# #endregion EXT:internal:MappingModels:Base\n" - }, - { - "contract_id": "EXT:internal:NavigationContracts", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 466, - "end_line": 468, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: NavigationContracts", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:NavigationContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: NavigationContracts\n# #endregion EXT:internal:NavigationContracts\n" - }, - { - "contract_id": "EXT:internal:PageContracts", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 470, - "end_line": 472, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: PageContracts", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:PageContracts [C:1] [TYPE External]\n# @BRIEF Internal module reference: PageContracts\n# #endregion EXT:internal:PageContracts\n" - }, - { - "contract_id": "EXT:internal:Permissions", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 474, - "end_line": 476, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal module reference: Permissions", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:Permissions [C:1] [TYPE External]\n# @BRIEF Internal module reference: Permissions\n# #endregion EXT:internal:Permissions\n" - }, - { - "contract_id": "EXT:method:AsyncAPIClient._handle_http_error", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 478, - "end_line": 480, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: AsyncAPIClient._handle_http_error", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:AsyncAPIClient._handle_http_error [C:1] [TYPE External]\n# @BRIEF Method reference: AsyncAPIClient._handle_http_error\n# #endregion EXT:method:AsyncAPIClient._handle_http_error\n" - }, - { - "contract_id": "EXT:method:AsyncAPIClient.request", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 482, - "end_line": 484, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: AsyncAPIClient.request", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:AsyncAPIClient.request [C:1] [TYPE External]\n# @BRIEF Method reference: AsyncAPIClient.request\n# #endregion EXT:method:AsyncAPIClient.request\n" - }, - { - "contract_id": "EXT:method:BackupPlugin:execute", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 486, - "end_line": 488, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: BackupPlugin:execute", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:BackupPlugin:execute [C:1] [TYPE External]\n# @BRIEF Method reference: BackupPlugin:execute\n# #endregion EXT:method:BackupPlugin:execute\n" - }, - { - "contract_id": "EXT:method:GlobalSettings.app_timezone", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 490, - "end_line": 492, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: GlobalSettings.app_timezone", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:GlobalSettings.app_timezone [C:1] [TYPE External]\n# @BRIEF Method reference: GlobalSettings.app_timezone\n# #endregion EXT:method:GlobalSettings.app_timezone\n" - }, - { - "contract_id": "EXT:method:MappingService:get_suggestions", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 494, - "end_line": 496, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: MappingService:get_suggestions", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:MappingService:get_suggestions [C:1] [TYPE External]\n# @BRIEF Method reference: MappingService:get_suggestions\n# #endregion EXT:method:MappingService:get_suggestions\n" - }, - { - "contract_id": "EXT:method:MigrationPlugin:execute", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 498, - "end_line": 500, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: MigrationPlugin:execute", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:MigrationPlugin:execute [C:1] [TYPE External]\n# @BRIEF Method reference: MigrationPlugin:execute\n# #endregion EXT:method:MigrationPlugin:execute\n" - }, - { - "contract_id": "EXT:method:SessionEventLogger.log_event", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 502, - "end_line": 504, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SessionEventLogger.log_event", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SessionEventLogger.log_event [C:1] [TYPE External]\n# @BRIEF Method reference: SessionEventLogger.log_event\n# #endregion EXT:method:SessionEventLogger.log_event\n" - }, - { - "contract_id": "EXT:method:SupersetClient.compile_dataset_preview", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 506, - "end_line": 508, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetClient.compile_dataset_preview", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetClient.compile_dataset_preview [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.compile_dataset_preview\n# #endregion EXT:method:SupersetClient.compile_dataset_preview\n" - }, - { - "contract_id": "EXT:method:SupersetClient.get_dashboard", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 510, - "end_line": 512, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetClient.get_dashboard", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetClient.get_dashboard [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dashboard\n# #endregion EXT:method:SupersetClient.get_dashboard\n" - }, - { - "contract_id": "EXT:method:SupersetClient.get_dashboard_detail", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 514, - "end_line": 516, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetClient.get_dashboard_detail", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetClient.get_dashboard_detail [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dashboard_detail\n# #endregion EXT:method:SupersetClient.get_dashboard_detail\n" - }, - { - "contract_id": "EXT:method:SupersetClient.get_dashboards_summary", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 518, - "end_line": 520, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetClient.get_dashboards_summary", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetClient.get_dashboards_summary [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dashboards_summary\n# #endregion EXT:method:SupersetClient.get_dashboards_summary\n" - }, - { - "contract_id": "EXT:method:SupersetClient.get_dataset", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 522, - "end_line": 524, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetClient.get_dataset", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetClient.get_dataset [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.get_dataset\n# #endregion EXT:method:SupersetClient.get_dataset\n" - }, - { - "contract_id": "EXT:method:SupersetClient.update_dataset", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 526, - "end_line": 528, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetClient.update_dataset", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetClient.update_dataset [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetClient.update_dataset\n# #endregion EXT:method:SupersetClient.update_dataset\n" - }, - { - "contract_id": "EXT:method:SupersetContextExtractor.parse_superset_link", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 530, - "end_line": 532, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: SupersetContextExtractor.parse_superset_link", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:SupersetContextExtractor.parse_superset_link [C:1] [TYPE External]\n# @BRIEF Method reference: SupersetContextExtractor.parse_superset_link\n# #endregion EXT:method:SupersetContextExtractor.parse_superset_link\n" - }, - { - "contract_id": "EXT:method:TASK_TYPE_PLUGIN_MAP", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 534, - "end_line": 536, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TASK_TYPE_PLUGIN_MAP", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TASK_TYPE_PLUGIN_MAP [C:1] [TYPE External]\n# @BRIEF Method reference: TASK_TYPE_PLUGIN_MAP\n# #endregion EXT:method:TASK_TYPE_PLUGIN_MAP\n" - }, - { - "contract_id": "EXT:method:TaskLogPersistenceService.add_logs", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 538, - "end_line": 540, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TaskLogPersistenceService.add_logs", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TaskLogPersistenceService.add_logs [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.add_logs\n# #endregion EXT:method:TaskLogPersistenceService.add_logs\n" - }, - { - "contract_id": "EXT:method:TaskLogPersistenceService.delete_logs_for_tasks", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 542, - "end_line": 544, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TaskLogPersistenceService.delete_logs_for_tasks", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TaskLogPersistenceService.delete_logs_for_tasks [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.delete_logs_for_tasks\n# #endregion EXT:method:TaskLogPersistenceService.delete_logs_for_tasks\n" - }, - { - "contract_id": "EXT:method:TaskLogPersistenceService.get_log_stats", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 546, - "end_line": 548, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TaskLogPersistenceService.get_log_stats", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TaskLogPersistenceService.get_log_stats [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.get_log_stats\n# #endregion EXT:method:TaskLogPersistenceService.get_log_stats\n" - }, - { - "contract_id": "EXT:method:TaskLogPersistenceService.get_logs", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 550, - "end_line": 552, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TaskLogPersistenceService.get_logs", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TaskLogPersistenceService.get_logs [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.get_logs\n# #endregion EXT:method:TaskLogPersistenceService.get_logs\n" - }, - { - "contract_id": "EXT:method:TaskLogPersistenceService.get_sources", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 554, - "end_line": 556, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TaskLogPersistenceService.get_sources", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TaskLogPersistenceService.get_sources [C:1] [TYPE External]\n# @BRIEF Method reference: TaskLogPersistenceService.get_sources\n# #endregion EXT:method:TaskLogPersistenceService.get_sources\n" - }, - { - "contract_id": "EXT:method:TaskManager.create_task", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 558, - "end_line": 560, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TaskManager.create_task", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TaskManager.create_task [C:1] [TYPE External]\n# @BRIEF Method reference: TaskManager.create_task\n# #endregion EXT:method:TaskManager.create_task\n" - }, - { - "contract_id": "EXT:method:TranslationExecutor._auto_size_batches", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 562, - "end_line": 564, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: TranslationExecutor._auto_size_batches", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:TranslationExecutor._auto_size_batches [C:1] [TYPE External]\n# @BRIEF Method reference: TranslationExecutor._auto_size_batches\n# #endregion EXT:method:TranslationExecutor._auto_size_batches\n" - }, - { - "contract_id": "EXT:method:_build_body", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 566, - "end_line": 568, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _build_body", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_build_body [C:1] [TYPE External]\n# @BRIEF Method reference: _build_body\n# #endregion EXT:method:_build_body\n" - }, - { - "contract_id": "EXT:method:_extract_resource_name_from_task", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 570, - "end_line": 572, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _extract_resource_name_from_task", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_extract_resource_name_from_task [C:1] [TYPE External]\n# @BRIEF Method reference: _extract_resource_name_from_task\n# #endregion EXT:method:_extract_resource_name_from_task\n" - }, - { - "contract_id": "EXT:method:_extract_resource_type_from_task", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 574, - "end_line": 576, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _extract_resource_type_from_task", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_extract_resource_type_from_task [C:1] [TYPE External]\n# @BRIEF Method reference: _extract_resource_type_from_task\n# #endregion EXT:method:_extract_resource_type_from_task\n" - }, - { - "contract_id": "EXT:method:_find_dashboard_owners", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 578, - "end_line": 580, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _find_dashboard_owners", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_find_dashboard_owners [C:1] [TYPE External]\n# @BRIEF Method reference: _find_dashboard_owners\n# #endregion EXT:method:_find_dashboard_owners\n" - }, - { - "contract_id": "EXT:method:_get_git_status_for_dashboard", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 582, - "end_line": 584, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _get_git_status_for_dashboard", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_get_git_status_for_dashboard [C:1] [TYPE External]\n# @BRIEF Method reference: _get_git_status_for_dashboard\n# #endregion EXT:method:_get_git_status_for_dashboard\n" - }, - { - "contract_id": "EXT:method:_get_last_llm_task_for_dashboard", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 586, - "end_line": 588, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _get_last_llm_task_for_dashboard", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_get_last_llm_task_for_dashboard [C:1] [TYPE External]\n# @BRIEF Method reference: _get_last_llm_task_for_dashboard\n# #endregion EXT:method:_get_last_llm_task_for_dashboard\n" - }, - { - "contract_id": "EXT:method:_get_last_task_for_resource", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 590, - "end_line": 592, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _get_last_task_for_resource", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_get_last_task_for_resource [C:1] [TYPE External]\n# @BRIEF Method reference: _get_last_task_for_resource\n# #endregion EXT:method:_get_last_task_for_resource\n" - }, - { - "contract_id": "EXT:method:_initialize_providers", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 594, - "end_line": 596, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _initialize_providers", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_initialize_providers [C:1] [TYPE External]\n# @BRIEF Method reference: _initialize_providers\n# #endregion EXT:method:_initialize_providers\n" - }, - { - "contract_id": "EXT:method:_llm_http", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 598, - "end_line": 600, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _llm_http", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_llm_http [C:1] [TYPE External]\n# @BRIEF Method reference: _llm_http\n# #endregion EXT:method:_llm_http\n" - }, - { - "contract_id": "EXT:method:_normalize_datetime_for_compare", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 602, - "end_line": 604, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _normalize_datetime_for_compare", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_normalize_datetime_for_compare [C:1] [TYPE External]\n# @BRIEF Method reference: _normalize_datetime_for_compare\n# #endregion EXT:method:_normalize_datetime_for_compare\n" - }, - { - "contract_id": "EXT:method:_normalize_task_status", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 606, - "end_line": 608, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _normalize_task_status", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_normalize_task_status [C:1] [TYPE External]\n# @BRIEF Method reference: _normalize_task_status\n# #endregion EXT:method:_normalize_task_status\n" - }, - { - "contract_id": "EXT:method:_normalize_validation_status", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 610, - "end_line": 612, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _normalize_validation_status", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_normalize_validation_status [C:1] [TYPE External]\n# @BRIEF Method reference: _normalize_validation_status\n# #endregion EXT:method:_normalize_validation_status\n" - }, - { - "contract_id": "EXT:method:_prime_dashboard_meta_cache", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 614, - "end_line": 616, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _prime_dashboard_meta_cache", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_prime_dashboard_meta_cache [C:1] [TYPE External]\n# @BRIEF Method reference: _prime_dashboard_meta_cache\n# #endregion EXT:method:_prime_dashboard_meta_cache\n" - }, - { - "contract_id": "EXT:method:_resolve_dashboard_meta", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 618, - "end_line": 620, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _resolve_dashboard_meta", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_resolve_dashboard_meta [C:1] [TYPE External]\n# @BRIEF Method reference: _resolve_dashboard_meta\n# #endregion EXT:method:_resolve_dashboard_meta\n" - }, - { - "contract_id": "EXT:method:_resolve_targets", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 622, - "end_line": 624, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _resolve_targets", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_resolve_targets [C:1] [TYPE External]\n# @BRIEF Method reference: _resolve_targets\n# #endregion EXT:method:_resolve_targets\n" - }, - { - "contract_id": "EXT:method:_should_notify", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 626, - "end_line": 628, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: _should_notify", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:_should_notify [C:1] [TYPE External]\n# @BRIEF Method reference: _should_notify\n# #endregion EXT:method:_should_notify\n" - }, - { - "contract_id": "EXT:method:decrypt", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 630, - "end_line": 632, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: decrypt", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:decrypt [C:1] [TYPE External]\n# @BRIEF Method reference: decrypt\n# #endregion EXT:method:decrypt\n" - }, - { - "contract_id": "EXT:method:encrypt", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 634, - "end_line": 636, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: encrypt", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:encrypt [C:1] [TYPE External]\n# @BRIEF Method reference: encrypt\n# #endregion EXT:method:encrypt\n" - }, - { - "contract_id": "EXT:method:get_activity_summary", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 638, - "end_line": 640, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: get_activity_summary", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:get_activity_summary [C:1] [TYPE External]\n# @BRIEF Method reference: get_activity_summary\n# #endregion EXT:method:get_activity_summary\n" - }, - { - "contract_id": "EXT:method:get_dashboards_with_status", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 642, - "end_line": 644, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: get_dashboards_with_status", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:get_dashboards_with_status [C:1] [TYPE External]\n# @BRIEF Method reference: get_dashboards_with_status\n# #endregion EXT:method:get_dashboards_with_status\n" - }, - { - "contract_id": "EXT:method:get_datasets_with_status", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 646, - "end_line": 648, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: get_datasets_with_status", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:get_datasets_with_status [C:1] [TYPE External]\n# @BRIEF Method reference: get_datasets_with_status\n# #endregion EXT:method:get_datasets_with_status\n" - }, - { - "contract_id": "EXT:method:get_repo", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 650, - "end_line": 652, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: get_repo", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:get_repo [C:1] [TYPE External]\n# @BRIEF Method reference: get_repo\n# #endregion EXT:method:get_repo\n" - }, - { - "contract_id": "EXT:method:schemas.translate.TargetSchemaValidationRequest", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 654, - "end_line": 656, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: schemas.translate.TargetSchemaValidationRequest", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:schemas.translate.TargetSchemaValidationRequest [C:1] [TYPE External]\n# @BRIEF Method reference: schemas.translate.TargetSchemaValidationRequest\n# #endregion EXT:method:schemas.translate.TargetSchemaValidationRequest\n" - }, - { - "contract_id": "EXT:method:schemas.translate.TargetSchemaValidationResponse", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 658, - "end_line": 660, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: schemas.translate.TargetSchemaValidationResponse", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:schemas.translate.TargetSchemaValidationResponse [C:1] [TYPE External]\n# @BRIEF Method reference: schemas.translate.TargetSchemaValidationResponse\n# #endregion EXT:method:schemas.translate.TargetSchemaValidationResponse\n" - }, - { - "contract_id": "EXT:method:self.network.request", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 662, - "end_line": 664, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: self.network.request", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:self.network.request [C:1] [TYPE External]\n# @BRIEF Method reference: self.network.request\n# #endregion EXT:method:self.network.request\n" - }, - { - "contract_id": "EXT:module:TargetName", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 666, - "end_line": 668, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: TargetName", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:module:TargetName [C:1] [TYPE External]\n# @BRIEF External reference: TargetName\n# #endregion EXT:module:TargetName\n" - }, - { - "contract_id": "EXT:path:EnvSelector.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 670, - "end_line": 672, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: EnvSelector.svelte", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:EnvSelector.svelte [C:1] [TYPE External]\n# @BRIEF File path: EnvSelector.svelte\n# #endregion EXT:path:EnvSelector.svelte\n" - }, - { - "contract_id": "EXT:path:MappingTable.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 674, - "end_line": 676, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: MappingTable.svelte", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:MappingTable.svelte [C:1] [TYPE External]\n# @BRIEF File path: MappingTable.svelte\n# #endregion EXT:path:MappingTable.svelte\n" - }, - { - "contract_id": "EXT:path:backend/requirements.txt", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 678, - "end_line": 680, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: backend/requirements.txt", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend/requirements.txt [C:1] [TYPE External]\n# @BRIEF File path: backend/requirements.txt\n# #endregion EXT:path:backend/requirements.txt\n" - }, - { - "contract_id": "EXT:path:docker/backend.entrypoint.sh", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 682, - "end_line": 684, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: docker/backend.entrypoint.sh", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:docker/backend.entrypoint.sh [C:1] [TYPE External]\n# @BRIEF File path: docker/backend.entrypoint.sh\n# #endregion EXT:path:docker/backend.entrypoint.sh\n" - }, - { - "contract_id": "EXT:path:docker/frontend.entrypoint.sh", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 686, - "end_line": 688, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: docker/frontend.entrypoint.sh", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:docker/frontend.entrypoint.sh [C:1] [TYPE External]\n# @BRIEF File path: docker/frontend.entrypoint.sh\n# #endregion EXT:path:docker/frontend.entrypoint.sh\n" - }, - { - "contract_id": "EXT:path:docker/nginx.conf", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 690, - "end_line": 692, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: docker/nginx.conf", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:docker/nginx.conf [C:1] [TYPE External]\n# @BRIEF File path: docker/nginx.conf\n# #endregion EXT:path:docker/nginx.conf\n" - }, - { - "contract_id": "EXT:path:docker/nginx.ssl.conf", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 694, - "end_line": 696, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: docker/nginx.ssl.conf", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:docker/nginx.ssl.conf [C:1] [TYPE External]\n# @BRIEF File path: docker/nginx.ssl.conf\n# #endregion EXT:path:docker/nginx.ssl.conf\n" - }, - { - "contract_id": "EXT:path:frontend/package.json", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 698, - "end_line": 700, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: frontend/package.json", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/package.json [C:1] [TYPE External]\n# @BRIEF File path: frontend/package.json\n# #endregion EXT:path:frontend/package.json\n" - }, - { - "contract_id": "EXT:path:frontend/src/components/EnvSelector.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 702, - "end_line": 704, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: frontend/src/components/EnvSelector.svelte", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/components/EnvSelector.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/EnvSelector.svelte\n# #endregion EXT:path:frontend/src/components/EnvSelector.svelte\n" - }, - { - "contract_id": "EXT:path:frontend/src/components/MappingTable.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 706, - "end_line": 708, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: frontend/src/components/MappingTable.svelte", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/components/MappingTable.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/MappingTable.svelte\n# #endregion EXT:path:frontend/src/components/MappingTable.svelte\n" - }, - { - "contract_id": "EXT:path:frontend/src/components/PasswordPrompt.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 710, - "end_line": 712, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: frontend/src/components/PasswordPrompt.svelte", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/components/PasswordPrompt.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/PasswordPrompt.svelte\n# #endregion EXT:path:frontend/src/components/PasswordPrompt.svelte\n" - }, - { - "contract_id": "EXT:path:frontend/src/components/TaskHistory.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 714, - "end_line": 716, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: frontend/src/components/TaskHistory.svelte", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/components/TaskHistory.svelte [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/components/TaskHistory.svelte\n# #endregion EXT:path:frontend/src/components/TaskHistory.svelte\n" - }, - { - "contract_id": "EXT:path:frontend/src/lib/api.js", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 718, - "end_line": 720, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "File path: frontend/src/lib/api.js", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/lib/api.js [C:1] [TYPE External]\n# @BRIEF File path: frontend/src/lib/api.js\n# #endregion EXT:path:frontend/src/lib/api.js\n" - }, - { - "contract_id": "EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 722, - "end_line": 724, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: SUPPORTED_DENSITIES", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES [C:1] [TYPE External]\n# @BRIEF External reference: SUPPORTED_DENSITIES\n# #endregion EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES\n" - }, - { - "contract_id": "EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 726, - "end_line": 728, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: SUPPORTED_START_PAGES", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES [C:1] [TYPE External]\n# @BRIEF External reference: SUPPORTED_START_PAGES\n# #endregion EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES\n" - }, - { - "contract_id": "EXT:requests", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 730, - "end_line": 732, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: requests", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:requests [C:1] [TYPE External]\n# @BRIEF External reference: requests\n# #endregion EXT:requests\n" - }, - { - "contract_id": "EXT:spec:BulkReplaceModal:Component", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 734, - "end_line": 736, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Spec/design-time contract: BulkReplaceModal:Component", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:spec:BulkReplaceModal:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: BulkReplaceModal:Component\n# #endregion EXT:spec:BulkReplaceModal:Component\n" - }, - { - "contract_id": "EXT:spec:CorrectionCell:Component", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 738, - "end_line": 740, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Spec/design-time contract: CorrectionCell:Component", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:spec:CorrectionCell:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: CorrectionCell:Component\n# #endregion EXT:spec:CorrectionCell:Component\n" - }, - { - "contract_id": "EXT:spec:StatsBar:Component", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 742, - "end_line": 744, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Spec/design-time contract: StatsBar:Component", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:spec:StatsBar:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: StatsBar:Component\n# #endregion EXT:spec:StatsBar:Component\n" - }, - { - "contract_id": "EXT:spec:TargetSchemaHint:Component", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 746, - "end_line": 748, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Spec/design-time contract: TargetSchemaHint:Component", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:spec:TargetSchemaHint:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: TargetSchemaHint:Component\n# #endregion EXT:spec:TargetSchemaHint:Component\n" - }, - { - "contract_id": "EXT:spec:TranslationPreview:Component", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 750, - "end_line": 752, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Spec/design-time contract: TranslationPreview:Component", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:spec:TranslationPreview:Component [C:1] [TYPE External]\n# @BRIEF Spec/design-time contract: TranslationPreview:Component\n# #endregion EXT:spec:TranslationPreview:Component\n" - }, - { - "contract_id": "EXT:ss-tools:log_security_event", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 754, - "end_line": 756, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External reference: log_security_event", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:ss-tools:log_security_event [C:1] [TYPE External]\n# @BRIEF External reference: log_security_event\n# #endregion EXT:ss-tools:log_security_event\n" - }, - { - "contract_id": "EXT:sveltekit:$app/environment", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 758, - "end_line": 760, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "SvelteKit import: $app/environment", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:sveltekit:$app/environment [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $app/environment\n# #endregion EXT:sveltekit:$app/environment\n" - }, - { - "contract_id": "EXT:sveltekit:$app/navigation", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 762, - "end_line": 764, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "SvelteKit import: $app/navigation", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:sveltekit:$app/navigation [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $app/navigation\n# #endregion EXT:sveltekit:$app/navigation\n" - }, - { - "contract_id": "EXT:sveltekit:$app/stores", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 766, - "end_line": 768, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "SvelteKit import: $app/stores", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:sveltekit:$app/stores [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $app/stores\n# #endregion EXT:sveltekit:$app/stores\n" - }, - { - "contract_id": "EXT:sveltekit:$env/static/public", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 770, - "end_line": 772, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "SvelteKit import: $env/static/public", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:sveltekit:$env/static/public [C:1] [TYPE External]\n# @BRIEF SvelteKit import: $env/static/public\n# #endregion EXT:sveltekit:$env/static/public\n" - }, - { - "contract_id": "EXT:list:GitDashboardPage_GitConfigRoutes", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 775, - "end_line": 777, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "E2E test list target: GitDashboardPage, GitConfigRoutes", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:GitDashboardPage_GitConfigRoutes [C:1] [TYPE External]\n# @BRIEF E2E test list target: GitDashboardPage, GitConfigRoutes\n# #endregion EXT:list:GitDashboardPage_GitConfigRoutes\n" - }, - { - "contract_id": "EXT:list:MigrationApi_SettingsPage", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 779, - "end_line": 781, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "E2E test list target: MigrationApi, SettingsPage", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:MigrationApi_SettingsPage [C:1] [TYPE External]\n# @BRIEF E2E test list target: MigrationApi, SettingsPage\n# #endregion EXT:list:MigrationApi_SettingsPage\n" - }, - { - "contract_id": "EXT:list:LoginPage_SettingsPage_TranslateJob_GitConfig", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 783, - "end_line": 785, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "E2E test list target: LoginPage, SettingsPage, TranslateJob, GitConfig", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:LoginPage_SettingsPage_TranslateJob_GitConfig [C:1] [TYPE External]\n# @BRIEF E2E test list target: LoginPage, SettingsPage, TranslateJob, GitConfig\n# #endregion EXT:list:LoginPage_SettingsPage_TranslateJob_GitConfig\n" - }, - { - "contract_id": "EXT:list:TranslatePage_TranslateJobRoutes", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 787, - "end_line": 789, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "E2E test list target: TranslatePage, TranslateJobRoutes", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:TranslatePage_TranslateJobRoutes [C:1] [TYPE External]\n# @BRIEF E2E test list target: TranslatePage, TranslateJobRoutes\n# #endregion EXT:list:TranslatePage_TranslateJobRoutes\n" - }, - { - "contract_id": "EXT:list:DashboardHub_LLM_SettingsPage", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 791, - "end_line": 793, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "E2E test list target: DashboardHub, LLM, SettingsPage", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:DashboardHub_LLM_SettingsPage [C:1] [TYPE External]\n# @BRIEF E2E test list target: DashboardHub, LLM, SettingsPage\n# #endregion EXT:list:DashboardHub_LLM_SettingsPage\n" - }, - { - "contract_id": "EXT:list:StartupEnvironmentWizard_LoginPage_EnvironmentsTab", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 795, - "end_line": 797, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "E2E test list target: StartupEnvironmentWizard, LoginPage, EnvironmentsTab", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:StartupEnvironmentWizard_LoginPage_EnvironmentsTab [C:1] [TYPE External]\n# @BRIEF E2E test list target: StartupEnvironmentWizard, LoginPage, EnvironmentsTab\n# #endregion EXT:list:StartupEnvironmentWizard_LoginPage_EnvironmentsTab\n" - }, - { - "contract_id": "EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 799, - "end_line": 801, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Test filter state references", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge [C:1] [TYPE External]\n# @BRIEF Test filter state references\n# #endregion EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge\n" - }, - { - "contract_id": "EXT:list:MigrationEngine_internal_methods", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 803, - "end_line": 805, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "MigrationEngine internal method references", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:MigrationEngine_internal_methods [C:1] [TYPE External]\n# @BRIEF MigrationEngine internal method references\n# #endregion EXT:list:MigrationEngine_internal_methods\n" - }, - { - "contract_id": "EXT:list:MigrateEngine_transform_methods", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 807, - "end_line": 809, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "MigrationEngine transform method references", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:MigrateEngine_transform_methods [C:1] [TYPE External]\n# @BRIEF MigrationEngine transform method references\n# #endregion EXT:list:MigrateEngine_transform_methods\n" - }, - { - "contract_id": "EXT:list:GitPackage_all_routes", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 811, - "end_line": 813, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Git package all route references", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:list:GitPackage_all_routes [C:1] [TYPE External]\n# @BRIEF Git package all route references\n# #endregion EXT:list:GitPackage_all_routes\n" - }, - { - "contract_id": "EXT:internal:_text_cleaner", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 815, - "end_line": 817, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Test module reference: _text_cleaner", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:_text_cleaner [C:1] [TYPE External]\n# @BRIEF Test module reference: _text_cleaner\n# #endregion EXT:internal:_text_cleaner\n" - }, - { - "contract_id": "EXT:internal:_utils", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 819, - "end_line": 821, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Test module reference: _utils", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:_utils [C:1] [TYPE External]\n# @BRIEF Test module reference: _utils\n# #endregion EXT:internal:_utils\n" - }, - { - "contract_id": "EXT:internal:test_health_service", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 823, - "end_line": 825, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Test self-reference: test_health_service", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:test_health_service [C:1] [TYPE External]\n# @BRIEF Test self-reference: test_health_service\n# #endregion EXT:internal:test_health_service\n" - }, - { - "contract_id": "EXT:internal:re_sqlparse", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 827, - "end_line": 829, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal: re and sqlparse", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:re_sqlparse [C:1] [TYPE External]\n# @BRIEF Internal: re and sqlparse\n# #endregion EXT:internal:re_sqlparse\n" - }, - { - "contract_id": "EXT:method:ValidationApi.fetchTasks", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 831, - "end_line": 833, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: ValidationApi.fetchTasks", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:ValidationApi.fetchTasks [C:1] [TYPE External]\n# @BRIEF Method reference: ValidationApi.fetchTasks\n# #endregion EXT:method:ValidationApi.fetchTasks\n" - }, - { - "contract_id": "EXT:frontend:ReportTypeProfiles", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 835, - "end_line": 837, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: ReportTypeProfiles", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ReportTypeProfiles [C:1] [TYPE External]\n# @BRIEF Frontend: ReportTypeProfiles\n# #endregion EXT:frontend:ReportTypeProfiles\n" - }, - { - "contract_id": "EXT:frontend:AssistantChatTest", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 839, - "end_line": 841, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: AssistantChatTest", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:AssistantChatTest [C:1] [TYPE External]\n# @BRIEF Frontend: AssistantChatTest\n# #endregion EXT:frontend:AssistantChatTest\n" - }, - { - "contract_id": "EXT:frontend:DashboardDetailPage", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 843, - "end_line": 845, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: DashboardDetailPage", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DashboardDetailPage [C:1] [TYPE External]\n# @BRIEF Frontend: DashboardDetailPage\n# #endregion EXT:frontend:DashboardDetailPage\n" - }, - { - "contract_id": "EXT:frontend:DashboardRouter", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 847, - "end_line": 849, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: DashboardRouter", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DashboardRouter [C:1] [TYPE External]\n# @BRIEF Frontend: DashboardRouter\n# #endregion EXT:frontend:DashboardRouter\n" - }, - { - "contract_id": "EXT:frontend:EnvConfig", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 851, - "end_line": 853, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: EnvConfig", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:EnvConfig [C:1] [TYPE External]\n# @BRIEF Frontend: EnvConfig\n# #endregion EXT:frontend:EnvConfig\n" - }, - { - "contract_id": "EXT:frontend:App", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 855, - "end_line": 857, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend module: App", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:App [C:1] [TYPE External]\n# @BRIEF Frontend module: App\n# #endregion EXT:frontend:App\n" - }, - { - "contract_id": "EXT:frontend:Utils", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 859, - "end_line": 861, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend utility module: Utils", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:Utils [C:1] [TYPE External]\n# @BRIEF Frontend utility module: Utils\n# #endregion EXT:frontend:Utils\n" - }, - { - "contract_id": "EXT:frontend:AuthService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 863, - "end_line": 865, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: AuthService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:AuthService [C:1] [TYPE External]\n# @BRIEF Frontend: AuthService\n# #endregion EXT:frontend:AuthService\n" - }, - { - "contract_id": "EXT:frontend:Navbar", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 867, - "end_line": 869, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend component: Navbar", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:Navbar [C:1] [TYPE External]\n# @BRIEF Frontend component: Navbar\n# #endregion EXT:frontend:Navbar\n" - }, - { - "contract_id": "EXT:frontend:ValidationPolicy", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 871, - "end_line": 873, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: ValidationPolicy", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:ValidationPolicy [C:1] [TYPE External]\n# @BRIEF Frontend: ValidationPolicy\n# #endregion EXT:frontend:ValidationPolicy\n" - }, - { - "contract_id": "EXT:frontend:Select", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 875, - "end_line": 877, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend component: Select", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:Select [C:1] [TYPE External]\n# @BRIEF Frontend component: Select\n# #endregion EXT:frontend:Select\n" - }, - { - "contract_id": "EXT:frontend:AssistantApi", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 879, - "end_line": 881, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: AssistantApi", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:AssistantApi [C:1] [TYPE External]\n# @BRIEF Frontend: AssistantApi\n# #endregion EXT:frontend:AssistantApi\n" - }, - { - "contract_id": "EXT:frontend:DatasetReviewApi", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 883, - "end_line": 885, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: DatasetReviewApi", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DatasetReviewApi [C:1] [TYPE External]\n# @BRIEF Frontend: DatasetReviewApi\n# #endregion EXT:frontend:DatasetReviewApi\n" - }, - { - "contract_id": "EXT:frontend:DatasetReviewWorkspace", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 887, - "end_line": 889, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: DatasetReviewWorkspace", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DatasetReviewWorkspace [C:1] [TYPE External]\n# @BRIEF Frontend: DatasetReviewWorkspace\n# #endregion EXT:frontend:DatasetReviewWorkspace\n" - }, - { - "contract_id": "EXT:frontend:auth", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 891, - "end_line": 893, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: auth", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:auth [C:1] [TYPE External]\n# @BRIEF Frontend: auth\n# #endregion EXT:frontend:auth\n" - }, - { - "contract_id": "EXT:Library:OAuth", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 895, - "end_line": 897, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: OAuth", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:OAuth [C:1] [TYPE External]\n# @BRIEF External library: OAuth\n# #endregion EXT:Library:OAuth\n" - }, - { - "contract_id": "EXT:frontend:adminService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 899, - "end_line": 901, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend service: adminService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:adminService [C:1] [TYPE External]\n# @BRIEF Frontend service: adminService\n# #endregion EXT:frontend:adminService\n" - }, - { - "contract_id": "EXT:path:backend.src.api.routes.migration", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 903, - "end_line": 905, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API route: migration", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.api.routes.migration [C:1] [TYPE External]\n# @BRIEF Backend API route: migration\n# #endregion EXT:path:backend.src.api.routes.migration\n" - }, - { - "contract_id": "EXT:path:backend.src.core.plugin_base.PluginBase", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 907, - "end_line": 909, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend core: PluginBase", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.core.plugin_base.PluginBase [C:1] [TYPE External]\n# @BRIEF Backend core: PluginBase\n# #endregion EXT:path:backend.src.core.plugin_base.PluginBase\n" - }, - { - "contract_id": "EXT:path:backend.src.models.clean_release", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 911, - "end_line": 913, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend model: clean_release", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.models.clean_release [C:1] [TYPE External]\n# @BRIEF Backend model: clean_release\n# #endregion EXT:path:backend.src.models.clean_release\n" - }, - { - "contract_id": "EXT:path:backend.src.models.git", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 915, - "end_line": 917, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend model: git", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.models.git [C:1] [TYPE External]\n# @BRIEF Backend model: git\n# #endregion EXT:path:backend.src.models.git\n" - }, - { - "contract_id": "EXT:path:backend.src.models.report", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 919, - "end_line": 921, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend model: report", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.models.report [C:1] [TYPE External]\n# @BRIEF Backend model: report\n# #endregion EXT:path:backend.src.models.report\n" - }, - { - "contract_id": "EXT:path:backend.api.storage", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 923, - "end_line": 925, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API: storage", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.api.storage [C:1] [TYPE External]\n# @BRIEF Backend API: storage\n# #endregion EXT:path:backend.api.storage\n" - }, - { - "contract_id": "EXT:path:backend.src.services.clean_release.demo_data_service", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 927, - "end_line": 929, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend service: demo_data_service", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.services.clean_release.demo_data_service [C:1] [TYPE External]\n# @BRIEF Backend service: demo_data_service\n# #endregion EXT:path:backend.src.services.clean_release.demo_data_service\n" - }, - { - "contract_id": "EXT:path:backend.src.services.clean_release.repository", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 931, - "end_line": 933, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend service: clean_release repository", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.services.clean_release.repository [C:1] [TYPE External]\n# @BRIEF Backend service: clean_release repository\n# #endregion EXT:path:backend.src.services.clean_release.repository\n" - }, - { - "contract_id": "EXT:path:backend.src.services.health_service.HealthService", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 935, - "end_line": 937, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend service: HealthService", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.services.health_service.HealthService [C:1] [TYPE External]\n# @BRIEF Backend service: HealthService\n# #endregion EXT:path:backend.src.services.health_service.HealthService\n" - }, - { - "contract_id": "EXT:path:backend.src.api.routes.admin.create_user", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 939, - "end_line": 941, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API route: admin.create_user", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.api.routes.admin.create_user [C:1] [TYPE External]\n# @BRIEF Backend API route: admin.create_user\n# #endregion EXT:path:backend.src.api.routes.admin.create_user\n" - }, - { - "contract_id": "EXT:path:backend.src.api.routes.admin.list_roles", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 943, - "end_line": 945, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API route: admin.list_roles", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.api.routes.admin.list_roles [C:1] [TYPE External]\n# @BRIEF Backend API route: admin.list_roles\n# #endregion EXT:path:backend.src.api.routes.admin.list_roles\n" - }, - { - "contract_id": "EXT:path:backend.src.api.routes.admin.list_users", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 947, - "end_line": 949, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API route: admin.list_users", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.api.routes.admin.list_users [C:1] [TYPE External]\n# @BRIEF Backend API route: admin.list_users\n# #endregion EXT:path:backend.src.api.routes.admin.list_users\n" - }, - { - "contract_id": "EXT:path:backend.src.api.routes.settings.get_logging_config", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 951, - "end_line": 953, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API route: settings.get_logging_config", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.api.routes.settings.get_logging_config [C:1] [TYPE External]\n# @BRIEF Backend API route: settings.get_logging_config\n# #endregion EXT:path:backend.src.api.routes.settings.get_logging_config\n" - }, - { - "contract_id": "EXT:path:backend.src.api.routes.settings.update_logging_config", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 955, - "end_line": 957, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend API route: settings.update_logging_config", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend.src.api.routes.settings.update_logging_config [C:1] [TYPE External]\n# @BRIEF Backend API route: settings.update_logging_config\n# #endregion EXT:path:backend.src.api.routes.settings.update_logging_config\n" - }, - { - "contract_id": "EXT:path:backend/src/plugins/llm_analysis/plugin.py", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 959, - "end_line": 961, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Backend plugin path: llm_analysis/plugin.py", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:backend/src/plugins/llm_analysis/plugin.py [C:1] [TYPE External]\n# @BRIEF Backend plugin path: llm_analysis/plugin.py\n# #endregion EXT:path:backend/src/plugins/llm_analysis/plugin.py\n" - }, - { - "contract_id": "EXT:path:frontend/src/routes/reports/llm/[taskId]/+page.svelte", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 967, - "end_line": 969, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend route path", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/routes/reports/llm/[taskId]/+page.svelte [C:1] [TYPE External]\n# @BRIEF Frontend route path\n# #endregion EXT:path:frontend/src/routes/reports/llm/[taskId]/+page.svelte\n" - }, - { - "contract_id": "EXT:path:frontend/src/services/toolsService.js", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 971, - "end_line": 973, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend service path: toolsService.js", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:frontend/src/services/toolsService.js [C:1] [TYPE External]\n# @BRIEF Frontend service path: toolsService.js\n# #endregion EXT:path:frontend/src/services/toolsService.js\n" - }, - { - "contract_id": "EXT:path:../task_logger.py", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 979, - "end_line": 981, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Relative path: task_logger.py", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:../task_logger.py [C:1] [TYPE External]\n# @BRIEF Relative path: task_logger.py\n# #endregion EXT:path:../task_logger.py\n" - }, - { - "contract_id": "EXT:path:/tools/storage", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 983, - "end_line": 985, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Path: /tools/storage", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:/tools/storage [C:1] [TYPE External]\n# @BRIEF Path: /tools/storage\n# #endregion EXT:path:/tools/storage\n" - }, - { - "contract_id": "EXT:path:src/core/logger.py", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 987, - "end_line": 989, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Path: src/core/logger.py", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:src/core/logger.py [C:1] [TYPE External]\n# @BRIEF Path: src/core/logger.py\n# #endregion EXT:path:src/core/logger.py\n" - }, - { - "contract_id": "EXT:path:src.core.logger", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 991, - "end_line": 993, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Path: src.core.logger", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:src.core.logger [C:1] [TYPE External]\n# @BRIEF Path: src.core.logger\n# #endregion EXT:path:src.core.logger\n" - }, - { - "contract_id": "EXT:path:src.core.plugin_base.PluginBase", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 995, - "end_line": 997, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Path: src.core.plugin_base.PluginBase", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:path:src.core.plugin_base.PluginBase [C:1] [TYPE External]\n# @BRIEF Path: src.core.plugin_base.PluginBase\n# #endregion EXT:path:src.core.plugin_base.PluginBase\n" - }, - { - "contract_id": "EXT:frontend:GitServiceClient", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 999, - "end_line": 1001, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: GitServiceClient", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:GitServiceClient [C:1] [TYPE External]\n# @BRIEF Frontend: GitServiceClient\n# #endregion EXT:frontend:GitServiceClient\n" - }, - { - "contract_id": "EXT:Library:sqlalchemy.Session", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1003, - "end_line": 1005, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: sqlalchemy.Session", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:sqlalchemy.Session [C:1] [TYPE External]\n# @BRIEF External library: sqlalchemy.Session\n# #endregion EXT:Library:sqlalchemy.Session\n" - }, - { - "contract_id": "EXT:internal:OAuth", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1007, - "end_line": 1009, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Internal: OAuth", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:internal:OAuth [C:1] [TYPE External]\n# @BRIEF Internal: OAuth\n# #endregion EXT:internal:OAuth\n" - }, - { - "contract_id": "EXT:frontend:BackupManager", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1047, - "end_line": 1049, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend component: BackupManager", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:BackupManager [C:1] [TYPE External]\n# @BRIEF Frontend component: BackupManager\n# #endregion EXT:frontend:BackupManager\n" - }, - { - "contract_id": "EXT:frontend:DebugTool", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1051, - "end_line": 1053, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend component: DebugTool", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DebugTool [C:1] [TYPE External]\n# @BRIEF Frontend component: DebugTool\n# #endregion EXT:frontend:DebugTool\n" - }, - { - "contract_id": "EXT:frontend:toasts", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1055, - "end_line": 1057, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store: toasts", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:toasts [C:1] [TYPE External]\n# @BRIEF Frontend store: toasts\n# #endregion EXT:frontend:toasts\n" - }, - { - "contract_id": "EXT:frontend:onresume", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1059, - "end_line": 1061, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend event: onresume", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:onresume [C:1] [TYPE External]\n# @BRIEF Frontend event: onresume\n# #endregion EXT:frontend:onresume\n" - }, - { - "contract_id": "EXT:frontend:oncancel", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1063, - "end_line": 1065, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend event: oncancel", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:oncancel [C:1] [TYPE External]\n# @BRIEF Frontend event: oncancel\n# #endregion EXT:frontend:oncancel\n" - }, - { - "contract_id": "EXT:frontend:onresolve", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1067, - "end_line": 1069, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend event: onresolve", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:onresolve [C:1] [TYPE External]\n# @BRIEF Frontend event: onresolve\n# #endregion EXT:frontend:onresolve\n" - }, - { - "contract_id": "EXT:frontend:api.js", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1071, - "end_line": 1073, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend module: api.js", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:api.js [C:1] [TYPE External]\n# @BRIEF Frontend module: api.js\n# #endregion EXT:frontend:api.js\n" - }, - { - "contract_id": "EXT:frontend:i18n.locale", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1075, - "end_line": 1077, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store: i18n.locale", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:i18n.locale [C:1] [TYPE External]\n# @BRIEF Frontend store: i18n.locale\n# #endregion EXT:frontend:i18n.locale\n" - }, - { - "contract_id": "EXT:frontend:DashboardMaintenanceBadge", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1079, - "end_line": 1081, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend component: DashboardMaintenanceBadge", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:DashboardMaintenanceBadge [C:1] [TYPE External]\n# @BRIEF Frontend component: DashboardMaintenanceBadge\n# #endregion EXT:frontend:DashboardMaintenanceBadge\n" - }, - { - "contract_id": "EXT:frontend:locale", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1083, - "end_line": 1085, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend store: locale", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:locale [C:1] [TYPE External]\n# @BRIEF Frontend store: locale\n# #endregion EXT:frontend:locale\n" - }, - { - "contract_id": "EXT:frontend:TaskDrawer", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1087, - "end_line": 1089, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend component: TaskDrawer", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:TaskDrawer [C:1] [TYPE External]\n# @BRIEF Frontend component: TaskDrawer\n# #endregion EXT:frontend:TaskDrawer\n" - }, - { - "contract_id": "EXT:Library:superset_tool.client", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1091, - "end_line": 1093, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: superset_tool.client", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:superset_tool.client [C:1] [TYPE External]\n# @BRIEF External library: superset_tool.client\n# #endregion EXT:Library:superset_tool.client\n" - }, - { - "contract_id": "EXT:Library:superset_tool.utils", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1095, - "end_line": 1097, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: superset_tool.utils", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:superset_tool.utils [C:1] [TYPE External]\n# @BRIEF External library: superset_tool.utils\n# #endregion EXT:Library:superset_tool.utils\n" - }, - { - "contract_id": "EXT:Library:pandas", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1099, - "end_line": 1101, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: pandas", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:pandas [C:1] [TYPE External]\n# @BRIEF External library: pandas\n# #endregion EXT:Library:pandas\n" - }, - { - "contract_id": "EXT:Library:bcrypt", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1103, - "end_line": 1105, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: bcrypt", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:bcrypt [C:1] [TYPE External]\n# @BRIEF External library: bcrypt\n# #endregion EXT:Library:bcrypt\n" - }, - { - "contract_id": "EXT:Library:rapidfuzz", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1107, - "end_line": 1109, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: rapidfuzz", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:rapidfuzz [C:1] [TYPE External]\n# @BRIEF External library: rapidfuzz\n# #endregion EXT:Library:rapidfuzz\n" - }, - { - "contract_id": "EXT:Library:tenacity", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1111, - "end_line": 1113, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: tenacity", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:tenacity [C:1] [TYPE External]\n# @BRIEF External library: tenacity\n# #endregion EXT:Library:tenacity\n" - }, - { - "contract_id": "EXT:Library:OAuth2PasswordBearer", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1115, - "end_line": 1117, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: OAuth2PasswordBearer", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:OAuth2PasswordBearer [C:1] [TYPE External]\n# @BRIEF External library: OAuth2PasswordBearer\n# #endregion EXT:Library:OAuth2PasswordBearer\n" - }, - { - "contract_id": "EXT:Library:pydantic_settings.BaseSettings", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1119, - "end_line": 1121, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "External library: pydantic_settings.BaseSettings", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Library:pydantic_settings.BaseSettings [C:1] [TYPE External]\n# @BRIEF External library: pydantic_settings.BaseSettings\n# #endregion EXT:Library:pydantic_settings.BaseSettings\n" - }, - { - "contract_id": "EXT:Python:enum", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1123, - "end_line": 1125, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Python standard library: enum", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:Python:enum [C:1] [TYPE External]\n# @BRIEF Python standard library: enum\n# #endregion EXT:Python:enum\n" - }, - { - "contract_id": "EXT:frontend:i18n_ru_locale", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1135, - "end_line": 1137, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: i18n_ru_locale", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:i18n_ru_locale [C:1] [TYPE External]\n# @BRIEF Frontend: i18n_ru_locale\n# #endregion EXT:frontend:i18n_ru_locale\n" - }, - { - "contract_id": "EXT:frontend:i18n_en_locale", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1139, - "end_line": 1141, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Frontend: i18n_en_locale", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:frontend:i18n_en_locale [C:1] [TYPE External]\n# @BRIEF Frontend: i18n_en_locale\n# #endregion EXT:frontend:i18n_en_locale\n" - }, - { - "contract_id": "EXT:method:adminService.createADGroupMapping", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1143, - "end_line": 1145, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: adminService.createADGroupMapping", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:adminService.createADGroupMapping [C:1] [TYPE External]\n# @BRIEF Method reference: adminService.createADGroupMapping\n# #endregion EXT:method:adminService.createADGroupMapping\n" - }, - { - "contract_id": "EXT:method:adminService.getLoggingConfig", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1147, - "end_line": 1149, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: adminService.getLoggingConfig", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:adminService.getLoggingConfig [C:1] [TYPE External]\n# @BRIEF Method reference: adminService.getLoggingConfig\n# #endregion EXT:method:adminService.getLoggingConfig\n" - }, - { - "contract_id": "EXT:method:adminService.updateLoggingConfig", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1151, - "end_line": 1153, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: adminService.updateLoggingConfig", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:adminService.updateLoggingConfig [C:1] [TYPE External]\n# @BRIEF Method reference: adminService.updateLoggingConfig\n# #endregion EXT:method:adminService.updateLoggingConfig\n" - }, - { - "contract_id": "EXT:method:adminService.deleteUser", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1155, - "end_line": 1157, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: adminService.deleteUser", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:adminService.deleteUser [C:1] [TYPE External]\n# @BRIEF Method reference: adminService.deleteUser\n# #endregion EXT:method:adminService.deleteUser\n" - }, - { - "contract_id": "EXT:method:adminService.createUser", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1159, - "end_line": 1161, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: adminService.createUser", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:adminService.createUser [C:1] [TYPE External]\n# @BRIEF Method reference: adminService.createUser\n# #endregion EXT:method:adminService.createUser\n" - }, - { - "contract_id": "EXT:method:adminService.updateUser", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1163, - "end_line": 1165, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: adminService.updateUser", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:adminService.updateUser [C:1] [TYPE External]\n# @BRIEF Method reference: adminService.updateUser\n# #endregion EXT:method:adminService.updateUser\n" - }, - { - "contract_id": "EXT:method:DatasetMapper.get_sqllab_mappings", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1167, - "end_line": 1169, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: DatasetMapper.get_sqllab_mappings", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:DatasetMapper.get_sqllab_mappings [C:1] [TYPE External]\n# @BRIEF Method reference: DatasetMapper.get_sqllab_mappings\n# #endregion EXT:method:DatasetMapper.get_sqllab_mappings\n" - }, - { - "contract_id": "EXT:method:DatasetMapper.load_excel_mappings", - "contract_type": "External", - "file_path": "backend/src/schemas/_external_stubs.py", - "start_line": 1171, - "end_line": 1173, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "BRIEF": "Method reference: DatasetMapper.load_excel_mappings", - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region EXT:method:DatasetMapper.load_excel_mappings [C:1] [TYPE External]\n# @BRIEF Method reference: DatasetMapper.load_excel_mappings\n# #endregion EXT:method:DatasetMapper.load_excel_mappings\n" - }, { "contract_id": "AuthSchemas", "contract_type": "Module", @@ -47754,20 +42932,20 @@ { "source_id": "candidate_service", "relation_type": "DEPENDS_ON", - "target_id": "EXT:path:backend.src.services.clean_release.repository", - "target_ref": "[EXT:path:backend.src.services.clean_release.repository]" + "target_id": "CleanReleaseRepository", + "target_ref": "[CleanReleaseRepository]" }, { "source_id": "candidate_service", "relation_type": "DEPENDS_ON", - "target_id": "EXT:path:backend.src.models.clean_release", - "target_ref": "[EXT:path:backend.src.models.clean_release]" + "target_id": "CleanReleaseModels", + "target_ref": "[CleanReleaseModels]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region candidate_service [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, candidate, lifecycle, release]\n# @BRIEF Register release candidates with validated artifacts and advance lifecycle through legal transitions.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:path:backend.src.services.clean_release.repository]\n# @RELATION DEPENDS_ON -> [EXT:path:backend.src.models.clean_release]\n# @PRE candidate_id must be unique; artifacts input must be non-empty and valid.\n# @POST candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only.\n# @INVARIANT Candidate lifecycle transitions are delegated to domain guard logic.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...models.clean_release import CandidateArtifact, ReleaseCandidate\nfrom .enums import CandidateStatus\nfrom .repository import CleanReleaseRepository\n\n\n# #region _validate_artifacts [TYPE Function]\n# @BRIEF Validate raw artifact payload list for required fields and shape.\n# @PRE artifacts payload is provided by caller.\n# @POST Returns normalized artifact list or raises ValueError.\ndef _validate_artifacts(artifacts: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:\n normalized = list(artifacts)\n if not normalized:\n raise ValueError(\"artifacts must not be empty\")\n\n required_fields = (\"id\", \"path\", \"sha256\", \"size\")\n for index, artifact in enumerate(normalized):\n if not isinstance(artifact, dict):\n raise ValueError(f\"artifact[{index}] must be an object\")\n for field in required_fields:\n if field not in artifact:\n raise ValueError(f\"artifact[{index}] missing required field '{field}'\")\n if not str(artifact[\"id\"]).strip():\n raise ValueError(f\"artifact[{index}] field 'id' must be non-empty\")\n if not str(artifact[\"path\"]).strip():\n raise ValueError(f\"artifact[{index}] field 'path' must be non-empty\")\n if not str(artifact[\"sha256\"]).strip():\n raise ValueError(f\"artifact[{index}] field 'sha256' must be non-empty\")\n if not isinstance(artifact[\"size\"], int) or artifact[\"size\"] <= 0:\n raise ValueError(f\"artifact[{index}] field 'size' must be a positive integer\")\n return normalized\n# #endregion _validate_artifacts\n\n\n# #region register_candidate [TYPE Function]\n# @BRIEF Register a candidate and persist its artifacts with legal lifecycle transition.\n# @PRE candidate_id must be unique and artifacts must pass validation.\n# @POST Candidate exists in repository with PREPARED status and artifacts persisted.\ndef register_candidate(\n repository: CleanReleaseRepository,\n candidate_id: str,\n version: str,\n source_snapshot_ref: str,\n created_by: str,\n artifacts: Iterable[dict[str, Any]],\n) -> ReleaseCandidate:\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id must be non-empty\")\n if not version or not version.strip():\n raise ValueError(\"version must be non-empty\")\n if not source_snapshot_ref or not source_snapshot_ref.strip():\n raise ValueError(\"source_snapshot_ref must be non-empty\")\n if not created_by or not created_by.strip():\n raise ValueError(\"created_by must be non-empty\")\n\n existing = repository.get_candidate(candidate_id)\n if existing is not None:\n raise ValueError(f\"candidate '{candidate_id}' already exists\")\n\n validated_artifacts = _validate_artifacts(artifacts)\n\n candidate = ReleaseCandidate(\n id=candidate_id,\n version=version,\n source_snapshot_ref=source_snapshot_ref,\n created_by=created_by,\n created_at=datetime.now(UTC),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n\n for artifact_payload in validated_artifacts:\n artifact = CandidateArtifact(\n id=str(artifact_payload[\"id\"]),\n candidate_id=candidate_id,\n path=str(artifact_payload[\"path\"]),\n sha256=str(artifact_payload[\"sha256\"]),\n size=int(artifact_payload[\"size\"]),\n detected_category=artifact_payload.get(\"detected_category\"),\n declared_category=artifact_payload.get(\"declared_category\"),\n source_uri=artifact_payload.get(\"source_uri\"),\n source_host=artifact_payload.get(\"source_host\"),\n metadata_json=artifact_payload.get(\"metadata_json\", {}),\n )\n repository.save_artifact(artifact)\n\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n return candidate\n# #endregion register_candidate\n\n# #endregion candidate_service\n" + "body": "# #region candidate_service [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, candidate, lifecycle, release]\n# @BRIEF Register release candidates with validated artifacts and advance lifecycle through legal transitions.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @PRE candidate_id must be unique; artifacts input must be non-empty and valid.\n# @POST candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only.\n# @INVARIANT Candidate lifecycle transitions are delegated to domain guard logic.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...models.clean_release import CandidateArtifact, ReleaseCandidate\nfrom .enums import CandidateStatus\nfrom .repository import CleanReleaseRepository\n\n\n# #region _validate_artifacts [TYPE Function]\n# @BRIEF Validate raw artifact payload list for required fields and shape.\n# @PRE artifacts payload is provided by caller.\n# @POST Returns normalized artifact list or raises ValueError.\ndef _validate_artifacts(artifacts: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:\n normalized = list(artifacts)\n if not normalized:\n raise ValueError(\"artifacts must not be empty\")\n\n required_fields = (\"id\", \"path\", \"sha256\", \"size\")\n for index, artifact in enumerate(normalized):\n if not isinstance(artifact, dict):\n raise ValueError(f\"artifact[{index}] must be an object\")\n for field in required_fields:\n if field not in artifact:\n raise ValueError(f\"artifact[{index}] missing required field '{field}'\")\n if not str(artifact[\"id\"]).strip():\n raise ValueError(f\"artifact[{index}] field 'id' must be non-empty\")\n if not str(artifact[\"path\"]).strip():\n raise ValueError(f\"artifact[{index}] field 'path' must be non-empty\")\n if not str(artifact[\"sha256\"]).strip():\n raise ValueError(f\"artifact[{index}] field 'sha256' must be non-empty\")\n if not isinstance(artifact[\"size\"], int) or artifact[\"size\"] <= 0:\n raise ValueError(f\"artifact[{index}] field 'size' must be a positive integer\")\n return normalized\n# #endregion _validate_artifacts\n\n\n# #region register_candidate [TYPE Function]\n# @BRIEF Register a candidate and persist its artifacts with legal lifecycle transition.\n# @PRE candidate_id must be unique and artifacts must pass validation.\n# @POST Candidate exists in repository with PREPARED status and artifacts persisted.\ndef register_candidate(\n repository: CleanReleaseRepository,\n candidate_id: str,\n version: str,\n source_snapshot_ref: str,\n created_by: str,\n artifacts: Iterable[dict[str, Any]],\n) -> ReleaseCandidate:\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id must be non-empty\")\n if not version or not version.strip():\n raise ValueError(\"version must be non-empty\")\n if not source_snapshot_ref or not source_snapshot_ref.strip():\n raise ValueError(\"source_snapshot_ref must be non-empty\")\n if not created_by or not created_by.strip():\n raise ValueError(\"created_by must be non-empty\")\n\n existing = repository.get_candidate(candidate_id)\n if existing is not None:\n raise ValueError(f\"candidate '{candidate_id}' already exists\")\n\n validated_artifacts = _validate_artifacts(artifacts)\n\n candidate = ReleaseCandidate(\n id=candidate_id,\n version=version,\n source_snapshot_ref=source_snapshot_ref,\n created_by=created_by,\n created_at=datetime.now(UTC),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n\n for artifact_payload in validated_artifacts:\n artifact = CandidateArtifact(\n id=str(artifact_payload[\"id\"]),\n candidate_id=candidate_id,\n path=str(artifact_payload[\"path\"]),\n sha256=str(artifact_payload[\"sha256\"]),\n size=int(artifact_payload[\"size\"]),\n detected_category=artifact_payload.get(\"detected_category\"),\n declared_category=artifact_payload.get(\"declared_category\"),\n source_uri=artifact_payload.get(\"source_uri\"),\n source_host=artifact_payload.get(\"source_host\"),\n metadata_json=artifact_payload.get(\"metadata_json\", {}),\n )\n repository.save_artifact(artifact)\n\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n return candidate\n# #endregion register_candidate\n\n# #endregion candidate_service\n" }, { "contract_id": "_validate_artifacts", @@ -52098,7 +47276,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 -> [profile[EXT:internal:_utils]]\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 .llm_provider import EncryptionManager\nfrom .profile[EXT:internal:_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 profile[EXT:internal:_utils].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 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 .llm_provider 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" }, { "contract_id": "ProfilePreferenceService", @@ -52137,7 +47315,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 profile[EXT:internal:_utils].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.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" }, { "contract_id": "get_my_preference", @@ -52224,14 +47402,14 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Delegate to profile[EXT:internal:_utils].build_default_preference.", + "BRIEF": "Delegate to ProfileUtils.build_default_preference.", "COMPLEXITY": 1 }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region _build_default_preference [C:1] [TYPE Function]\n # @BRIEF Delegate to profile[EXT:internal:_utils].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" + "body": " # #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" }, { "contract_id": "_get_preference_row", @@ -52285,7 +47463,7 @@ "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 -> [profile[EXT:internal:_utils]]\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 (profile[EXT:internal:_utils]) 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[EXT:internal:_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 -> [profile[EXT:internal:_utils]]\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 ..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" }, { "contract_id": "ProfileService", @@ -52303,10 +47481,10 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #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 -> [profile[EXT:internal:_utils]]\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" + "body": "# #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" }, { - "contract_id": "profile[EXT:internal:_utils]", + "contract_id": "ProfileUtils", "contract_type": "Module", "file_path": "backend/src/services/profile_utils.py", "start_line": 1, @@ -52321,7 +47499,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region profile[EXT:internal:_utils] [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 -> [EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES]\n# @RELATION DEPENDS_ON -> [EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES]\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 profile[EXT:internal:_utils]\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 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" }, { "contract_id": "ProfileValidationError", @@ -52553,20 +47731,20 @@ { "source_id": "validate_update_payload", "relation_type": "DEPENDS_ON", - "target_id": "EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES", - "target_ref": "[EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES]" + "target_id": "ProfileUtils", + "target_ref": "[ProfileUtils]" }, { "source_id": "validate_update_payload", "relation_type": "DEPENDS_ON", - "target_id": "EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES", - "target_ref": "[EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES]" + "target_id": "ProfileUtils", + "target_ref": "[ProfileUtils]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #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 -> [EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES]\n# @RELATION DEPENDS_ON -> [EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES]\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" + "body": "# #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" }, { "contract_id": "build_default_preference", @@ -53244,7 +48422,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region security_badge_service [C:4] [TYPE Module] [SEMANTICS profile,security,permissions,badges]\n# @BRIEF Builds security summary and permission badges for profile UI — role extraction,\n# permission pair collection, and catalog formatting.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [discover_declared_permissions]\n# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]\n# @RATIONALE Extracted from ProfileService to satisfy INV_7. Security badge construction\n# is a distinct concern with its own dependencies (discover_declared_permissions,\n# plugin_loader) and can be tested independently.\n# @REJECTED Keeping security badge logic inside ProfileService was rejected — it adds\n# plugin_loader coupling to preference operations that do not need it and\n# inflates the class past 150 lines.\n# @PRE Plugin loader may be None; user must have roles/permissions graph.\n# @POST Returns deterministic security projection for profile UI with sorted permissions.\n# @SIDE_EFFECT Reads plugin permissions via discover_declared_permissions when plugin_loader is set.\n\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import User\nfrom ..schemas.profile import ProfilePermissionState, ProfileSecuritySummary\nfrom .profile[EXT:internal:_utils] import sanitize_text\nfrom .rbac_permission_catalog import discover_declared_permissions\n\n\n# #region SecurityBadgeService [C:4] [TYPE Class] [SEMANTICS security,permission,badge,profile]\n# @BRIEF Builds security summary with role names and permission badges for profile UI display.\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION CALLS -> [discover_declared_permissions]\n# @PRE plugin_loader may be None for degraded permission discovery.\n# @POST build_security_summary returns ProfileSecuritySummary with deterministic sorted permissions.\nclass SecurityBadgeService:\n \"\"\"Builds security summary with role names and permission badges.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize service with optional plugin loader for permission discovery.\n def __init__(self, plugin_loader: Any = None):\n self.plugin_loader = plugin_loader\n # #endregion __init__\n\n # #region build_security_summary [TYPE Function]\n # @BRIEF Build read-only security snapshot with role and permission badges.\n # @PRE current_user is authenticated.\n # @POST Returns deterministic security projection for profile UI.\n def build_security_summary(self, current_user: User) -> ProfileSecuritySummary:\n with belief_scope(\"SecurityBadgeService.build_security_summary\"):\n role_names_set: set[str] = set()\n roles = getattr(current_user, \"roles\", []) or []\n for role in roles:\n normalized_role_name = sanitize_text(getattr(role, \"name\", None))\n if normalized_role_name:\n role_names_set.add(normalized_role_name)\n role_names = sorted(role_names_set)\n\n is_admin = any(str(role_name).lower() == \"admin\" for role_name in role_names)\n user_permission_pairs = self._collect_user_permission_pairs(current_user)\n\n declared_permission_pairs: set[tuple[str, str]] = set()\n try:\n discovered_permissions = discover_declared_permissions(\n plugin_loader=self.plugin_loader\n )\n for resource, action in discovered_permissions:\n normalized_resource = sanitize_text(resource)\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n declared_permission_pairs.add(\n (normalized_resource, normalized_action)\n )\n except Exception as discovery_error:\n logger.explore(\n \"Failed to build declared permission catalog\",\n extra={\"error\": str(discovery_error), \"src\": \"SecurityBadgeService.build_security_summary\"},\n )\n\n if not declared_permission_pairs:\n declared_permission_pairs = set(user_permission_pairs)\n\n sorted_permission_pairs = sorted(\n declared_permission_pairs,\n key=lambda pair: (pair[0], pair[1]),\n )\n permission_states = [\n ProfilePermissionState(\n key=self._format_permission_key(resource, action),\n allowed=bool(is_admin or (resource, action) in user_permission_pairs),\n )\n for resource, action in sorted_permission_pairs\n ]\n\n auth_source = sanitize_text(getattr(current_user, \"auth_source\", None))\n current_role = \"Admin\" if is_admin else (role_names[0] if role_names else None)\n\n return ProfileSecuritySummary(\n read_only=True,\n auth_source=auth_source,\n current_role=current_role,\n role_source=auth_source,\n roles=role_names,\n permissions=permission_states,\n )\n # #endregion build_security_summary\n\n # #region _collect_user_permission_pairs [TYPE Function]\n # @BRIEF Collect effective permission tuples from current user's roles.\n # @PRE current_user can include role/permission graph.\n # @POST Returns unique normalized (resource, ACTION) tuples.\n def _collect_user_permission_pairs(\n self, current_user: User\n ) -> set[tuple[str, str]]:\n collected: set[tuple[str, str]] = set()\n roles = getattr(current_user, \"roles\", []) or []\n for role in roles:\n permissions = getattr(role, \"permissions\", []) or []\n for permission in permissions:\n resource = sanitize_text(getattr(permission, \"resource\", None))\n action = str(getattr(permission, \"action\", \"\") or \"\").strip().upper()\n if resource and action:\n collected.add((resource, action))\n return collected\n # #endregion _collect_user_permission_pairs\n\n # #region _format_permission_key [C:1] [TYPE Function]\n # @BRIEF Convert normalized permission pair to compact UI key.\n def _format_permission_key(self, resource: str, action: str) -> str:\n normalized_resource = sanitize_text(resource) or \"\"\n normalized_action = str(action or \"\").strip().upper()\n if normalized_action == \"READ\":\n return normalized_resource\n return f\"{normalized_resource}:{normalized_action.lower()}\"\n # #endregion _format_permission_key\n# #endregion SecurityBadgeService\n# #endregion security_badge_service\n" + "body": "# #region security_badge_service [C:4] [TYPE Module] [SEMANTICS profile,security,permissions,badges]\n# @BRIEF Builds security summary and permission badges for profile UI — role extraction,\n# permission pair collection, and catalog formatting.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [discover_declared_permissions]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @RATIONALE Extracted from ProfileService to satisfy INV_7. Security badge construction\n# is a distinct concern with its own dependencies (discover_declared_permissions,\n# plugin_loader) and can be tested independently.\n# @REJECTED Keeping security badge logic inside ProfileService was rejected — it adds\n# plugin_loader coupling to preference operations that do not need it and\n# inflates the class past 150 lines.\n# @PRE Plugin loader may be None; user must have roles/permissions graph.\n# @POST Returns deterministic security projection for profile UI with sorted permissions.\n# @SIDE_EFFECT Reads plugin permissions via discover_declared_permissions when plugin_loader is set.\n\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import User\nfrom ..schemas.profile import ProfilePermissionState, ProfileSecuritySummary\nfrom .profile_utils import sanitize_text\nfrom .rbac_permission_catalog import discover_declared_permissions\n\n\n# #region SecurityBadgeService [C:4] [TYPE Class] [SEMANTICS security,permission,badge,profile]\n# @BRIEF Builds security summary with role names and permission badges for profile UI display.\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION CALLS -> [discover_declared_permissions]\n# @PRE plugin_loader may be None for degraded permission discovery.\n# @POST build_security_summary returns ProfileSecuritySummary with deterministic sorted permissions.\nclass SecurityBadgeService:\n \"\"\"Builds security summary with role names and permission badges.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize service with optional plugin loader for permission discovery.\n def __init__(self, plugin_loader: Any = None):\n self.plugin_loader = plugin_loader\n # #endregion __init__\n\n # #region build_security_summary [TYPE Function]\n # @BRIEF Build read-only security snapshot with role and permission badges.\n # @PRE current_user is authenticated.\n # @POST Returns deterministic security projection for profile UI.\n def build_security_summary(self, current_user: User) -> ProfileSecuritySummary:\n with belief_scope(\"SecurityBadgeService.build_security_summary\"):\n role_names_set: set[str] = set()\n roles = getattr(current_user, \"roles\", []) or []\n for role in roles:\n normalized_role_name = sanitize_text(getattr(role, \"name\", None))\n if normalized_role_name:\n role_names_set.add(normalized_role_name)\n role_names = sorted(role_names_set)\n\n is_admin = any(str(role_name).lower() == \"admin\" for role_name in role_names)\n user_permission_pairs = self._collect_user_permission_pairs(current_user)\n\n declared_permission_pairs: set[tuple[str, str]] = set()\n try:\n discovered_permissions = discover_declared_permissions(\n plugin_loader=self.plugin_loader\n )\n for resource, action in discovered_permissions:\n normalized_resource = sanitize_text(resource)\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n declared_permission_pairs.add(\n (normalized_resource, normalized_action)\n )\n except Exception as discovery_error:\n logger.explore(\n \"Failed to build declared permission catalog\",\n extra={\"error\": str(discovery_error), \"src\": \"SecurityBadgeService.build_security_summary\"},\n )\n\n if not declared_permission_pairs:\n declared_permission_pairs = set(user_permission_pairs)\n\n sorted_permission_pairs = sorted(\n declared_permission_pairs,\n key=lambda pair: (pair[0], pair[1]),\n )\n permission_states = [\n ProfilePermissionState(\n key=self._format_permission_key(resource, action),\n allowed=bool(is_admin or (resource, action) in user_permission_pairs),\n )\n for resource, action in sorted_permission_pairs\n ]\n\n auth_source = sanitize_text(getattr(current_user, \"auth_source\", None))\n current_role = \"Admin\" if is_admin else (role_names[0] if role_names else None)\n\n return ProfileSecuritySummary(\n read_only=True,\n auth_source=auth_source,\n current_role=current_role,\n role_source=auth_source,\n roles=role_names,\n permissions=permission_states,\n )\n # #endregion build_security_summary\n\n # #region _collect_user_permission_pairs [TYPE Function]\n # @BRIEF Collect effective permission tuples from current user's roles.\n # @PRE current_user can include role/permission graph.\n # @POST Returns unique normalized (resource, ACTION) tuples.\n def _collect_user_permission_pairs(\n self, current_user: User\n ) -> set[tuple[str, str]]:\n collected: set[tuple[str, str]] = set()\n roles = getattr(current_user, \"roles\", []) or []\n for role in roles:\n permissions = getattr(role, \"permissions\", []) or []\n for permission in permissions:\n resource = sanitize_text(getattr(permission, \"resource\", None))\n action = str(getattr(permission, \"action\", \"\") or \"\").strip().upper()\n if resource and action:\n collected.add((resource, action))\n return collected\n # #endregion _collect_user_permission_pairs\n\n # #region _format_permission_key [C:1] [TYPE Function]\n # @BRIEF Convert normalized permission pair to compact UI key.\n def _format_permission_key(self, resource: str, action: str) -> str:\n normalized_resource = sanitize_text(resource) or \"\"\n normalized_action = str(action or \"\").strip().upper()\n if normalized_action == \"READ\":\n return normalized_resource\n return f\"{normalized_resource}:{normalized_action.lower()}\"\n # #endregion _format_permission_key\n# #endregion SecurityBadgeService\n# #endregion security_badge_service\n" }, { "contract_id": "SecurityBadgeService", @@ -53351,7 +48529,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:internal:re_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\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" }, { "contract_id": "detect_jinja_spans", @@ -53498,14 +48676,14 @@ { "source_id": "superset_lookup_service", "relation_type": "DEPENDS_ON", - "target_id": "profile[EXT:internal:_utils]", - "target_ref": "[profile[EXT:internal:_utils]]" + "target_id": "ProfileUtils", + "target_ref": "[ProfileUtils]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region superset_lookup_service [C:4] [TYPE Module] [SEMANTICS superset,account,lookup,environment]\n# @BRIEF Environment-scoped Superset account lookup with degradation fallback for network failures.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]\n# @RATIONALE Extracted from ProfileService to satisfy INV_7. Superset account lookup has distinct\n# I/O patterns (external HTTP calls, environment resolution) that isolate cleanly from\n# preference persistence and security badge logic.\n# @REJECTED Keeping lookup inside ProfileService was rejected — it adds network I/O and\n# environment coupling to preference operations that only need DB access, and inflates\n# the class past 150 lines.\n# @PRE current_user is authenticated; environment_id must be resolvable.\n# @POST Returns success or degraded SupersetAccountLookupResponse with stable shape.\n# @SIDE_EFFECT Performs external Superset HTTP calls via SupersetClient + adapter.\n# @RAISES EnvironmentNotFoundError when environment_id is unknown.\n\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom ..schemas.profile import (\n SupersetAccountCandidate,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom .profile[EXT:internal:_utils] import EnvironmentNotFoundError\n\n\n# #region SupersetLookupService [C:4] [TYPE Class] [SEMANTICS superset,lookup,environment,degradation]\n# @BRIEF Resolves environments and queries Superset users in selected environment,\n# returning canonical account candidates with degradation fallback.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n# @RELATION CALLS -> [SupersetClient]\n# @PRE config_manager supports get_environments().\n# @POST lookup_superset_accounts returns success payload or degraded payload with warning.\nclass SupersetLookupService:\n \"\"\"Environment-scoped Superset account lookup with degradation fallback.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with config manager for environment resolution.\n def __init__(self, config_manager: Any):\n self.config_manager = config_manager\n # #endregion __init__\n\n # #region lookup_superset_accounts [TYPE Function]\n # @BRIEF Query Superset users in selected environment and project canonical account candidates.\n # @PRE current_user is authenticated and environment_id exists.\n # @POST Returns success payload or degraded payload with warning while preserving manual fallback.\n def lookup_superset_accounts(\n self,\n current_user: Any,\n request: SupersetAccountLookupRequest,\n ) -> SupersetAccountLookupResponse:\n with belief_scope(\n \"SupersetLookupService.lookup_superset_accounts\",\n f\"user_id={getattr(current_user, 'id', '?')}, environment_id={request.environment_id}\",\n ):\n environment = self._resolve_environment(request.environment_id)\n if environment is None:\n logger.explore(\"Lookup aborted: environment not found\")\n raise EnvironmentNotFoundError(\n f\"Environment '{request.environment_id}' not found\"\n )\n\n sort_column = str(request.sort_column or \"username\").strip().lower()\n sort_order = str(request.sort_order or \"desc\").strip().lower()\n allowed_columns = {\"username\", \"first_name\", \"last_name\", \"email\"}\n if sort_column not in allowed_columns:\n sort_column = \"username\"\n if sort_order not in {\"asc\", \"desc\"}:\n sort_order = \"desc\"\n\n logger.reflect(\n \"Normalized lookup request \"\n f\"(env={request.environment_id}, sort_column={sort_column}, sort_order={sort_order}, \"\n f\"page_index={request.page_index}, page_size={request.page_size}, \"\n f\"search={(request.search or '').strip()!r})\"\n )\n\n try:\n logger.reason(\"Performing Superset account lookup\")\n superset_client = SupersetClient(environment)\n adapter = SupersetAccountLookupAdapter(\n network_client=superset_client.network,\n environment_id=request.environment_id,\n )\n lookup_result = adapter.get_users_page(\n search=request.search,\n page_index=request.page_index,\n page_size=request.page_size,\n sort_column=sort_column,\n sort_order=sort_order,\n )\n items = [\n SupersetAccountCandidate.model_validate(item)\n for item in lookup_result.get(\"items\", [])\n ]\n return SupersetAccountLookupResponse(\n status=\"success\",\n environment_id=request.environment_id,\n page_index=request.page_index,\n page_size=request.page_size,\n total=max(int(lookup_result.get(\"total\", len(items))), 0),\n warning=None,\n items=items,\n )\n except Exception as exc:\n logger.explore(\n f\"Lookup degraded due to upstream error: {exc}\"\n )\n return SupersetAccountLookupResponse(\n status=\"degraded\",\n environment_id=request.environment_id,\n page_index=request.page_index,\n page_size=request.page_size,\n total=0,\n warning=(\n \"Cannot load Superset accounts for this environment right now. \"\n \"You can enter username manually.\"\n ),\n items=[],\n )\n # #endregion lookup_superset_accounts\n\n # #region _resolve_environment [C:2] [TYPE Function]\n # @BRIEF Resolve environment model from configured environments by id.\n # @PRE environment_id is provided.\n # @POST Returns environment object when found else None.\n def _resolve_environment(self, environment_id: str):\n environments = self.config_manager.get_environments()\n for env in environments:\n if str(getattr(env, \"id\", \"\")) == str(environment_id):\n return env\n return None\n # #endregion _resolve_environment\n# #endregion SupersetLookupService\n# #endregion superset_lookup_service\n" + "body": "# #region superset_lookup_service [C:4] [TYPE Module] [SEMANTICS superset,account,lookup,environment]\n# @BRIEF Environment-scoped Superset account lookup with degradation fallback for network failures.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @RATIONALE Extracted from ProfileService to satisfy INV_7. Superset account lookup has distinct\n# I/O patterns (external HTTP calls, environment resolution) that isolate cleanly from\n# preference persistence and security badge logic.\n# @REJECTED Keeping lookup inside ProfileService was rejected — it adds network I/O and\n# environment coupling to preference operations that only need DB access, and inflates\n# the class past 150 lines.\n# @PRE current_user is authenticated; environment_id must be resolvable.\n# @POST Returns success or degraded SupersetAccountLookupResponse with stable shape.\n# @SIDE_EFFECT Performs external Superset HTTP calls via SupersetClient + adapter.\n# @RAISES EnvironmentNotFoundError when environment_id is unknown.\n\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom ..schemas.profile import (\n SupersetAccountCandidate,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom .profile_utils import EnvironmentNotFoundError\n\n\n# #region SupersetLookupService [C:4] [TYPE Class] [SEMANTICS superset,lookup,environment,degradation]\n# @BRIEF Resolves environments and queries Superset users in selected environment,\n# returning canonical account candidates with degradation fallback.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n# @RELATION CALLS -> [SupersetClient]\n# @PRE config_manager supports get_environments().\n# @POST lookup_superset_accounts returns success payload or degraded payload with warning.\nclass SupersetLookupService:\n \"\"\"Environment-scoped Superset account lookup with degradation fallback.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with config manager for environment resolution.\n def __init__(self, config_manager: Any):\n self.config_manager = config_manager\n # #endregion __init__\n\n # #region lookup_superset_accounts [TYPE Function]\n # @BRIEF Query Superset users in selected environment and project canonical account candidates.\n # @PRE current_user is authenticated and environment_id exists.\n # @POST Returns success payload or degraded payload with warning while preserving manual fallback.\n def lookup_superset_accounts(\n self,\n current_user: Any,\n request: SupersetAccountLookupRequest,\n ) -> SupersetAccountLookupResponse:\n with belief_scope(\n \"SupersetLookupService.lookup_superset_accounts\",\n f\"user_id={getattr(current_user, 'id', '?')}, environment_id={request.environment_id}\",\n ):\n environment = self._resolve_environment(request.environment_id)\n if environment is None:\n logger.explore(\"Lookup aborted: environment not found\")\n raise EnvironmentNotFoundError(\n f\"Environment '{request.environment_id}' not found\"\n )\n\n sort_column = str(request.sort_column or \"username\").strip().lower()\n sort_order = str(request.sort_order or \"desc\").strip().lower()\n allowed_columns = {\"username\", \"first_name\", \"last_name\", \"email\"}\n if sort_column not in allowed_columns:\n sort_column = \"username\"\n if sort_order not in {\"asc\", \"desc\"}:\n sort_order = \"desc\"\n\n logger.reflect(\n \"Normalized lookup request \"\n f\"(env={request.environment_id}, sort_column={sort_column}, sort_order={sort_order}, \"\n f\"page_index={request.page_index}, page_size={request.page_size}, \"\n f\"search={(request.search or '').strip()!r})\"\n )\n\n try:\n logger.reason(\"Performing Superset account lookup\")\n superset_client = SupersetClient(environment)\n adapter = SupersetAccountLookupAdapter(\n network_client=superset_client.network,\n environment_id=request.environment_id,\n )\n lookup_result = adapter.get_users_page(\n search=request.search,\n page_index=request.page_index,\n page_size=request.page_size,\n sort_column=sort_column,\n sort_order=sort_order,\n )\n items = [\n SupersetAccountCandidate.model_validate(item)\n for item in lookup_result.get(\"items\", [])\n ]\n return SupersetAccountLookupResponse(\n status=\"success\",\n environment_id=request.environment_id,\n page_index=request.page_index,\n page_size=request.page_size,\n total=max(int(lookup_result.get(\"total\", len(items))), 0),\n warning=None,\n items=items,\n )\n except Exception as exc:\n logger.explore(\n f\"Lookup degraded due to upstream error: {exc}\"\n )\n return SupersetAccountLookupResponse(\n status=\"degraded\",\n environment_id=request.environment_id,\n page_index=request.page_index,\n page_size=request.page_size,\n total=0,\n warning=(\n \"Cannot load Superset accounts for this environment right now. \"\n \"You can enter username manually.\"\n ),\n items=[],\n )\n # #endregion lookup_superset_accounts\n\n # #region _resolve_environment [C:2] [TYPE Function]\n # @BRIEF Resolve environment model from configured environments by id.\n # @PRE environment_id is provided.\n # @POST Returns environment object when found else None.\n def _resolve_environment(self, environment_id: str):\n environments = self.config_manager.get_environments()\n for env in environments:\n if str(getattr(env, \"id\", \"\")) == str(environment_id):\n return env\n return None\n # #endregion _resolve_environment\n# #endregion SupersetLookupService\n# #endregion superset_lookup_service\n" }, { "contract_id": "SupersetLookupService", @@ -55958,14 +51136,14 @@ { "source_id": "TestDemoModeIsolation", "relation_type": "DEPENDS_ON", - "target_id": "EXT:path:backend.src.services.clean_release.demo_data_service", - "target_ref": "[EXT:path:backend.src.services.clean_release.demo_data_service]" + "target_id": "DemoDataService", + "target_ref": "[DemoDataService]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDemoModeIsolation [C:2] [TYPE Module]\n# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository\n# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.\n# @LAYER Tests\n# @RELATION DEPENDS_ON -> [EXT:path:backend.src.services.clean_release.demo_data_service]\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nfrom src.models.clean_release import ReleaseCandidate\nfrom src.services.clean_release.demo_data_service import (\n build_namespaced_id,\n create_isolated_repository,\n resolve_namespace,\n)\n\n\n# #region test_resolve_namespace_separates_demo_and_real [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.\n# @PRE: Mode names are provided as user/runtime strings.\n# @POST: Demo and real namespaces are different and stable.\ndef test_resolve_namespace_separates_demo_and_real() -> None:\n demo = resolve_namespace(\"demo\")\n real = resolve_namespace(\"real\")\n\n assert demo == \"clean-release:demo\"\n assert real == \"clean-release:real\"\n assert demo != real\n# #endregion test_resolve_namespace_separates_demo_and_real\n\n\n# #region test_build_namespaced_id_prevents_cross_mode_collisions [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.\n# @PRE: Same logical candidate id is used in two different namespaces.\n# @POST: Produced physical IDs differ by namespace prefix.\ndef test_build_namespaced_id_prevents_cross_mode_collisions() -> None:\n logical_id = \"2026.03.09-rc1\"\n demo_id = build_namespaced_id(resolve_namespace(\"demo\"), logical_id)\n real_id = build_namespaced_id(resolve_namespace(\"real\"), logical_id)\n\n assert demo_id != real_id\n assert demo_id.startswith(\"clean-release:demo::\")\n assert real_id.startswith(\"clean-release:real::\")\n# #endregion test_build_namespaced_id_prevents_cross_mode_collisions\n\n\n# #region test_create_isolated_repository_keeps_mode_data_separate [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# #endregion test_create_isolated_repository_keeps_mode_data_separate\n\n# #endregion TestDemoModeIsolation\n" + "body": "# #region TestDemoModeIsolation [C:2] [TYPE Module]\n# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository\n# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.\n# @LAYER Tests\n# @RELATION DEPENDS_ON -> [DemoDataService]\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nfrom src.models.clean_release import ReleaseCandidate\nfrom src.services.clean_release.demo_data_service import (\n build_namespaced_id,\n create_isolated_repository,\n resolve_namespace,\n)\n\n\n# #region test_resolve_namespace_separates_demo_and_real [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.\n# @PRE: Mode names are provided as user/runtime strings.\n# @POST: Demo and real namespaces are different and stable.\ndef test_resolve_namespace_separates_demo_and_real() -> None:\n demo = resolve_namespace(\"demo\")\n real = resolve_namespace(\"real\")\n\n assert demo == \"clean-release:demo\"\n assert real == \"clean-release:real\"\n assert demo != real\n# #endregion test_resolve_namespace_separates_demo_and_real\n\n\n# #region test_build_namespaced_id_prevents_cross_mode_collisions [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.\n# @PRE: Same logical candidate id is used in two different namespaces.\n# @POST: Produced physical IDs differ by namespace prefix.\ndef test_build_namespaced_id_prevents_cross_mode_collisions() -> None:\n logical_id = \"2026.03.09-rc1\"\n demo_id = build_namespaced_id(resolve_namespace(\"demo\"), logical_id)\n real_id = build_namespaced_id(resolve_namespace(\"real\"), logical_id)\n\n assert demo_id != real_id\n assert demo_id.startswith(\"clean-release:demo::\")\n assert real_id.startswith(\"clean-release:real::\")\n# #endregion test_build_namespaced_id_prevents_cross_mode_collisions\n\n\n# #region test_create_isolated_repository_keeps_mode_data_separate [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# #endregion test_create_isolated_repository_keeps_mode_data_separate\n\n# #endregion TestDemoModeIsolation\n" }, { "contract_id": "test_resolve_namespace_separates_demo_and_real", @@ -58112,7 +53290,7 @@ "body": " # #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" }, { - "contract_id": "test_layout[EXT:internal:_utils]", + "contract_id": "TestLayoutUtils", "contract_type": "TestModule", "file_path": "backend/tests/test_layout_utils.py", "start_line": 1, @@ -58127,7 +53305,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_layout[EXT:internal:_utils] [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[EXT:internal:_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 test_layout[EXT:internal:_utils]\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)\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" }, { "contract_id": "test_empty_content", @@ -58542,14 +53720,14 @@ { "source_id": "TestLogger", "relation_type": "BINDS_TO", - "target_id": "EXT:path:src/core/logger.py", - "target_ref": "[EXT:path:src/core/logger.py]" + "target_id": "LoggerModule", + "target_ref": "[LoggerModule]" } ], "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 -> [EXT:path:src/core/logger.py]\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\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" }, { "contract_id": "test_belief_scope_success_coherence", @@ -63570,7 +58748,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region EnterpriseCleanSetupE2E [C:4] [TYPE Test] [SEMANTICS e2e, enterprise-clean, setup, onboarding, environment]\n// @BRIEF Stage 1: E2E test for enterprise-clean empty image — first-run setup via StartupEnvironmentWizard.\n// @@RATIONALE All 4 tests pass after fixing: (1) button ambiguity — zero-state page and wizard modal both have 'Начать настройку' button, resolved by scoping to wizard modal via .fixed.inset-0.z-50; (2) wizard detection — changed from waitForSelector('text=...') to getByText() with regex for locale-robust matching; (3) form fill — connections.* i18n keys missing in Docker build, switched to placeholder-based locators scoped within wizard; (4) settings navigation — use authPage fixture for consistent login\n// @@REJECTED Using waitForSelector('text=...') fails for locale-sensitive text. getByLabel() with connections.* keys fails when i18n namespace missing. Using page.goto() with authPage fixture causes page reload issues\n\n// Validates: bundle deployment, PostgreSQL fresh DB, initial admin bootstrap, environment creation wizard.\n// @RELATION BINDS_TO -> [[EXT:list:StartupEnvironmentWizard_LoginPage_EnvironmentsTab]]\n// @RELATION DEPENDS_ON -> [ApiHelper]\n// @UX_STATE WizardIntro -> Wizard explains setup purpose with \"Start setup\" button.\n// @UX_STATE WizardForm -> Form collects environment ID, name, URL, username, password, stage.\n// @UX_STATE WizardSaving -> Submit disabled, progress label shown.\n// @UX_STATE EnvironmentCreated -> Wizard closes, environment appears in selector, dashboards load.\n// @RATIONALE Enterprise-clean bundle ships with an empty PostgreSQL. First login MUST trigger the\n// StartupEnvironmentWizard (blocking experience). Creating the first Superset environment is the\n// critical path — if this fails, the bundle is not deliverable.\n// @REJECTED Skipping wizard via API pre-config — must test the actual user-facing wizard UX.\n// @INVARIANT Wizard appears on first login when no environments exist.\n// @INVARIANT After env creation, dashboard hub loads without wizard.\n//\n// Usage:\n// Deploy enterprise-clean stack first (docker compose -f docker-compose.enterprise-clean.yml up -d)\n// Then run: npx playwright test --grep \"Enterprise Clean Setup\"\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiGet } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8000';\nconst SUPERSET_URL = 'https://ss-dev.bebesh.ru';\nconst SUPERSET_USER = 'admin';\nconst SUPERSET_PASS = 'payroll-unadorned-many';\n\ntest.describe('Enterprise Clean Setup — Stage 1: Empty Image', () => {\n\n\t// ── 1.1. FIRST LOGIN — WIZARD TRIGGER ─────────────────────\n\ttest('first login triggers StartupEnvironmentWizard on empty instance', async ({ page }) => {\n\t\tawait test.step('Login to clean instance', async () => {\n\t\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\t\t\tawait page.waitForSelector('form', { timeout: 15_000 });\n\t\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\t\tawait page.getByLabel(/Пароль|Password/).fill('admin123');\n\t\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\t\t\t// Should redirect away from /login\n\t\t\tawait page.waitForURL(/\\/(?!login)/, { timeout: 15_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Login successful');\n\t\t});\n\n\t\tawait test.step('Dashboard redirect triggers StartupEnvironmentWizard', async () => {\n\t\t\t// After login, the HomePage redirects to /dashboards.\n\t\t\t// If no environments exist, the StartupEnvironmentWizard should appear.\n\t\t\tawait page.waitForURL(/\\/dashboards/, { timeout: 15_000 });\n\n\t\t\t// Look for the wizard modal — it has \"Configure your first environment\" heading\n\t\t\tconst wizardVisible = await page.getByText(/Configure your first environment|Настройте|setup_title/)\n\t\t\t\t.isVisible({ timeout: 10_000 })\n\t\t\t\t.catch(() => false);\n\n\t\t\tif (!wizardVisible) {\n\t\t\t\t// Fallback: the wizard may be in Russian locale\n\t\t\t\tconst wizardFallback = await page.getByText(/первое окружение|first environment/)\n\t\t\t\t\t.isVisible({ timeout: 5_000 })\n\t\t\t\t\t.catch(() => false);\n\t\t\t\texpect(wizardFallback).toBeTruthy();\n\t\t\t} else {\n\t\t\t\texpect(wizardVisible).toBeTruthy();\n\t\t\t}\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard triggered (no environments found)');\n\t\t});\n\n\t\tawait test.step('Wizard intro screen shows \"Start setup\" button', async () => {\n\t\t\t// There are TWO buttons with \"Start setup\"/\"Начать настройку\" text:\n\t\t\t// 1. Zero-state button on the dashboard hub page (outside modal)\n\t\t\t// 2. Wizard modal button (inside .fixed.inset-0.z-50 overlay)\n\t\t\t// Scope to the wizard modal to avoid the zero-state button\n\t\t\tconst wizardModal = page.locator('.fixed.inset-0.z-50');\n\t\t\tawait expect(wizardModal.getByRole('button', { name: /Start setup|Начать/ })).toBeVisible({ timeout: 5_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard intro \"Start setup\" button visible');\n\t\t});\n\t});\n\n\t// ── 1.2. WIZARD — CREATE ENVIRONMENT ──────────────────────\n\ttest('StartupEnvironmentWizard creates first Superset environment', async ({ page }) => {\n\t\tawait test.step('Login and wait for wizard', async () => {\n\t\t\t// Use manual login same as test 1 (proven to show wizard)\n\t\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\t\t\tawait page.waitForSelector('form', { timeout: 15_000 });\n\t\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\t\tawait page.getByLabel(/Пароль|Password/).fill('admin123');\n\t\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\t\t\tawait page.waitForURL(/\\/(?!login)/, { timeout: 15_000 });\n\t\t\tawait page.waitForURL(/\\/dashboards/, { timeout: 15_000 });\n\n\t\t\t// Wait for wizard — use getByText regex (same approach as test 1 which works)\n\t\t\tconst wizardVisible = await page.getByText(/Configure your first environment|Настройте первое окружение|setup_title/)\n\t\t\t\t.isVisible({ timeout: 15_000 })\n\t\t\t\t.catch(() => false);\n\t\t\texpect(wizardVisible).toBeTruthy();\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Logged in, wizard visible');\n\t\t});\n\n\t\tawait test.step('Click \"Start setup\" to open form', async () => {\n\t\t\t// Scope to wizard modal to avoid the zero-state button outside\n\t\t\tconst wizardModal = page.locator('.fixed.inset-0.z-50');\n\t\t\tawait wizardModal.getByRole('button', { name: /Start setup|Начать/ }).click();\n\t\t\t// Form should now be visible — look for ID, URL, username, password fields\n\t\t\tawait expect(page.getByText(/ID/).first()).toBeVisible({ timeout: 5_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard form opened');\n\t\t});\n\n\t\tawait test.step('Fill environment form and create', async () => {\n\t\t\tconst wizard = page.locator('.fixed.inset-0.z-50');\n\n\t\t\t// Fill environment details for ss-dev.bebesh.ru\n\t\t\t// Note: connections.* i18n keys may be missing, so use placeholder-based locators\n\t\t\tawait wizard.getByPlaceholder('dev-superset').fill('ss-dev');\n\t\t\t// Name field — placeholder \"Development\", no accessible label\n\t\t\tawait wizard.getByPlaceholder('Development').fill('ss-dev');\n\t\t\t// URL field — placeholder \"https://superset.example.com\"\n\t\t\tawait wizard.getByPlaceholder(/superset\\.example\\.com/).fill(SUPERSET_URL);\n\t\t\t// Username field — placeholder \"admin\" (scope to wizard to avoid login field)\n\t\t\tawait wizard.getByPlaceholder('admin').fill(SUPERSET_USER);\n\t\t\t// Password field — use password input type\n\t\t\tawait wizard.locator('input[type=\"password\"]').fill(SUPERSET_PASS);\n\n\t\t\t// Click \"Create environment\" button\n\t\t\tawait wizard.getByRole('button', { name: /Create environment|Создать окружение/ }).click();\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Environment form submitted');\n\t\t});\n\n\t\tawait test.step('Wizard closes, dashboard hub loads without wizard', async () => {\n\t\t\t// After successful creation, wizard should close\n\t\t\t// Wait for wizard to disappear (either \"Configure your first environment\" gone, or dashboard content appears)\n\t\t\tawait page.waitForTimeout(3_000);\n\n\t\t\t// Verify wizard is closed\n\t\t\tconst wizardStillVisible = await page.getByText(/Configure your first environment/)\n\t\t\t\t.isVisible({ timeout: 3_000 })\n\t\t\t\t.catch(() => false);\n\t\t\texpect(wizardStillVisible).toBeFalsy();\n\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard closed after environment creation');\n\t\t});\n\t});\n\n\t// ── 1.3. VERIFY ENVIRONMENT PERSISTED ─────────────────────\n\ttest('environment persists after creation and appears in settings', async ({ authPage }) => {\n\t\tconst page = authPage;\n\t\tawait test.step('Navigate to settings environments tab', async () => {\n\t\t\t// authPage fixture already logged in; go to settings\n\t\t\tawait page.goto(`${FRONTEND_URL}/settings#/settings/environments`);\n\t\t\t// Wait for settings page to load — look for the environments page content\n\t\t\tawait page.waitForTimeout(2_000);\n\t\t\t// Try to find \"Окружения\" or \"Environments\" heading\n\t\t\tconst envHeadingVisible = await page.getByText(/Окружения|Environments|Environment/)\n\t\t\t\t.first()\n\t\t\t\t.isVisible({ timeout: 15_000 })\n\t\t\t\t.catch(() => false);\n\t\t\tconsole.log(`[E2E] ✅ Stage 1: Settings → Environments tab visible: ${envHeadingVisible}`);\n\t\t});\n\n\t\tawait test.step('Verify via API that environment is persisted', async () => {\n\t\t\tconst envs = await apiGet('/api/settings/environments');\n\t\t\tconst ssDevEnv = envs.find(e => e.id === 'ss-dev');\n\t\t\tif (!ssDevEnv) {\n\t\t\t\tconsole.log('[E2E] ⚠️ Stage 1: Environment ss-dev not found via API, may have failed to create');\n\t\t\t} else {\n\t\t\t\texpect(ssDevEnv).toBeDefined();\n\t\t\t\texpect(ssDevEnv.url).toContain('ss-dev.bebesh.ru');\n\t\t\t\texpect(ssDevEnv.username).toBe('admin');\n\t\t\t\tconsole.log(`[E2E] ✅ Stage 1: API confirms env ss-dev: ${ssDevEnv.url}`);\n\t\t\t}\n\t\t});\n\t});\n\n\t// ── 1.4. DASHBOARD HUB LOADS WITHOUT WIZARD ───────────────\n\ttest('dashboard hub loads normally after environment creation', async ({ authPage }) => {\n\t\tconst page = authPage;\n\t\tawait test.step('Navigate to dashboards', async () => {\n\t\t\t// authPage fixture already logged in; go to dashboards\n\t\t\tawait page.goto(`${FRONTEND_URL}/dashboards`);\n\t\t\tawait page.waitForURL(/\\/dashboards/, { timeout: 10_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Dashboard hub reached');\n\t\t});\n\n\t\tawait test.step('No wizard appears on subsequent visits', async () => {\n\t\t\t// Verify wizard is NOT present (if environment was created in previous tests)\n\t\t\tconst wizardPresent = await page.getByText(/Configure your first environment|Настройте первое окружение/)\n\t\t\t\t.isVisible({ timeout: 3_000 })\n\t\t\t\t.catch(() => false);\n\t\t\tif (wizardPresent) {\n\t\t\t\tconsole.log('[E2E] ⚠️ Stage 1: Wizard still visible — env was not created or creation failed');\n\t\t\t} else {\n\t\t\t\tconsole.log('[E2E] ✅ Stage 1: No wizard on repeat visit');\n\t\t\t}\n\t\t});\n\n\t\tawait test.step('Environment selector visible in header', async () => {\n\t\t\t// Environment selector should now show the ss-dev environment\n\t\t\tconst envSelectorVisible = await page.getByText(/ss-dev/).first()\n\t\t\t\t.isVisible({ timeout: 5_000 })\n\t\t\t\t.catch(() => false);\n\t\t\tconsole.log(`[E2E] ✅ Stage 1: Environment selector visible: ${envSelectorVisible}`);\n\t\t});\n\t});\n});\n// #endregion EnterpriseCleanSetupE2E\n" + "body": "// #region EnterpriseCleanSetupE2E [C:4] [TYPE Test] [SEMANTICS e2e, enterprise-clean, setup, onboarding, environment]\n// @BRIEF Stage 1: E2E test for enterprise-clean empty image — first-run setup via StartupEnvironmentWizard.\n// @@RATIONALE All 4 tests pass after fixing: (1) button ambiguity — zero-state page and wizard modal both have 'Начать настройку' button, resolved by scoping to wizard modal via .fixed.inset-0.z-50; (2) wizard detection — changed from waitForSelector('text=...') to getByText() with regex for locale-robust matching; (3) form fill — connections.* i18n keys missing in Docker build, switched to placeholder-based locators scoped within wizard; (4) settings navigation — use authPage fixture for consistent login\n// @@REJECTED Using waitForSelector('text=...') fails for locale-sensitive text. getByLabel() with connections.* keys fails when i18n namespace missing. Using page.goto() with authPage fixture causes page reload issues\n\n// Validates: bundle deployment, PostgreSQL fresh DB, initial admin bootstrap, environment creation wizard.\n// @RELATION BINDS_TO -> [StartupEnvironmentWizard]\n// @RELATION DEPENDS_ON -> [ApiHelper]\n// @UX_STATE WizardIntro -> Wizard explains setup purpose with \"Start setup\" button.\n// @UX_STATE WizardForm -> Form collects environment ID, name, URL, username, password, stage.\n// @UX_STATE WizardSaving -> Submit disabled, progress label shown.\n// @UX_STATE EnvironmentCreated -> Wizard closes, environment appears in selector, dashboards load.\n// @RATIONALE Enterprise-clean bundle ships with an empty PostgreSQL. First login MUST trigger the\n// StartupEnvironmentWizard (blocking experience). Creating the first Superset environment is the\n// critical path — if this fails, the bundle is not deliverable.\n// @REJECTED Skipping wizard via API pre-config — must test the actual user-facing wizard UX.\n// @INVARIANT Wizard appears on first login when no environments exist.\n// @INVARIANT After env creation, dashboard hub loads without wizard.\n//\n// Usage:\n// Deploy enterprise-clean stack first (docker compose -f docker-compose.enterprise-clean.yml up -d)\n// Then run: npx playwright test --grep \"Enterprise Clean Setup\"\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiGet } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8000';\nconst SUPERSET_URL = 'https://ss-dev.bebesh.ru';\nconst SUPERSET_USER = 'admin';\nconst SUPERSET_PASS = 'payroll-unadorned-many';\n\ntest.describe('Enterprise Clean Setup — Stage 1: Empty Image', () => {\n\n\t// ── 1.1. FIRST LOGIN — WIZARD TRIGGER ─────────────────────\n\ttest('first login triggers StartupEnvironmentWizard on empty instance', async ({ page }) => {\n\t\tawait test.step('Login to clean instance', async () => {\n\t\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\t\t\tawait page.waitForSelector('form', { timeout: 15_000 });\n\t\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\t\tawait page.getByLabel(/Пароль|Password/).fill('admin123');\n\t\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\t\t\t// Should redirect away from /login\n\t\t\tawait page.waitForURL(/\\/(?!login)/, { timeout: 15_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Login successful');\n\t\t});\n\n\t\tawait test.step('Dashboard redirect triggers StartupEnvironmentWizard', async () => {\n\t\t\t// After login, the HomePage redirects to /dashboards.\n\t\t\t// If no environments exist, the StartupEnvironmentWizard should appear.\n\t\t\tawait page.waitForURL(/\\/dashboards/, { timeout: 15_000 });\n\n\t\t\t// Look for the wizard modal — it has \"Configure your first environment\" heading\n\t\t\tconst wizardVisible = await page.getByText(/Configure your first environment|Настройте|setup_title/)\n\t\t\t\t.isVisible({ timeout: 10_000 })\n\t\t\t\t.catch(() => false);\n\n\t\t\tif (!wizardVisible) {\n\t\t\t\t// Fallback: the wizard may be in Russian locale\n\t\t\t\tconst wizardFallback = await page.getByText(/первое окружение|first environment/)\n\t\t\t\t\t.isVisible({ timeout: 5_000 })\n\t\t\t\t\t.catch(() => false);\n\t\t\t\texpect(wizardFallback).toBeTruthy();\n\t\t\t} else {\n\t\t\t\texpect(wizardVisible).toBeTruthy();\n\t\t\t}\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard triggered (no environments found)');\n\t\t});\n\n\t\tawait test.step('Wizard intro screen shows \"Start setup\" button', async () => {\n\t\t\t// There are TWO buttons with \"Start setup\"/\"Начать настройку\" text:\n\t\t\t// 1. Zero-state button on the dashboard hub page (outside modal)\n\t\t\t// 2. Wizard modal button (inside .fixed.inset-0.z-50 overlay)\n\t\t\t// Scope to the wizard modal to avoid the zero-state button\n\t\t\tconst wizardModal = page.locator('.fixed.inset-0.z-50');\n\t\t\tawait expect(wizardModal.getByRole('button', { name: /Start setup|Начать/ })).toBeVisible({ timeout: 5_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard intro \"Start setup\" button visible');\n\t\t});\n\t});\n\n\t// ── 1.2. WIZARD — CREATE ENVIRONMENT ──────────────────────\n\ttest('StartupEnvironmentWizard creates first Superset environment', async ({ page }) => {\n\t\tawait test.step('Login and wait for wizard', async () => {\n\t\t\t// Use manual login same as test 1 (proven to show wizard)\n\t\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\t\t\tawait page.waitForSelector('form', { timeout: 15_000 });\n\t\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\t\tawait page.getByLabel(/Пароль|Password/).fill('admin123');\n\t\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\t\t\tawait page.waitForURL(/\\/(?!login)/, { timeout: 15_000 });\n\t\t\tawait page.waitForURL(/\\/dashboards/, { timeout: 15_000 });\n\n\t\t\t// Wait for wizard — use getByText regex (same approach as test 1 which works)\n\t\t\tconst wizardVisible = await page.getByText(/Configure your first environment|Настройте первое окружение|setup_title/)\n\t\t\t\t.isVisible({ timeout: 15_000 })\n\t\t\t\t.catch(() => false);\n\t\t\texpect(wizardVisible).toBeTruthy();\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Logged in, wizard visible');\n\t\t});\n\n\t\tawait test.step('Click \"Start setup\" to open form', async () => {\n\t\t\t// Scope to wizard modal to avoid the zero-state button outside\n\t\t\tconst wizardModal = page.locator('.fixed.inset-0.z-50');\n\t\t\tawait wizardModal.getByRole('button', { name: /Start setup|Начать/ }).click();\n\t\t\t// Form should now be visible — look for ID, URL, username, password fields\n\t\t\tawait expect(page.getByText(/ID/).first()).toBeVisible({ timeout: 5_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard form opened');\n\t\t});\n\n\t\tawait test.step('Fill environment form and create', async () => {\n\t\t\tconst wizard = page.locator('.fixed.inset-0.z-50');\n\n\t\t\t// Fill environment details for ss-dev.bebesh.ru\n\t\t\t// Note: connections.* i18n keys may be missing, so use placeholder-based locators\n\t\t\tawait wizard.getByPlaceholder('dev-superset').fill('ss-dev');\n\t\t\t// Name field — placeholder \"Development\", no accessible label\n\t\t\tawait wizard.getByPlaceholder('Development').fill('ss-dev');\n\t\t\t// URL field — placeholder \"https://superset.example.com\"\n\t\t\tawait wizard.getByPlaceholder(/superset\\.example\\.com/).fill(SUPERSET_URL);\n\t\t\t// Username field — placeholder \"admin\" (scope to wizard to avoid login field)\n\t\t\tawait wizard.getByPlaceholder('admin').fill(SUPERSET_USER);\n\t\t\t// Password field — use password input type\n\t\t\tawait wizard.locator('input[type=\"password\"]').fill(SUPERSET_PASS);\n\n\t\t\t// Click \"Create environment\" button\n\t\t\tawait wizard.getByRole('button', { name: /Create environment|Создать окружение/ }).click();\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Environment form submitted');\n\t\t});\n\n\t\tawait test.step('Wizard closes, dashboard hub loads without wizard', async () => {\n\t\t\t// After successful creation, wizard should close\n\t\t\t// Wait for wizard to disappear (either \"Configure your first environment\" gone, or dashboard content appears)\n\t\t\tawait page.waitForTimeout(3_000);\n\n\t\t\t// Verify wizard is closed\n\t\t\tconst wizardStillVisible = await page.getByText(/Configure your first environment/)\n\t\t\t\t.isVisible({ timeout: 3_000 })\n\t\t\t\t.catch(() => false);\n\t\t\texpect(wizardStillVisible).toBeFalsy();\n\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Wizard closed after environment creation');\n\t\t});\n\t});\n\n\t// ── 1.3. VERIFY ENVIRONMENT PERSISTED ─────────────────────\n\ttest('environment persists after creation and appears in settings', async ({ authPage }) => {\n\t\tconst page = authPage;\n\t\tawait test.step('Navigate to settings environments tab', async () => {\n\t\t\t// authPage fixture already logged in; go to settings\n\t\t\tawait page.goto(`${FRONTEND_URL}/settings#/settings/environments`);\n\t\t\t// Wait for settings page to load — look for the environments page content\n\t\t\tawait page.waitForTimeout(2_000);\n\t\t\t// Try to find \"Окружения\" or \"Environments\" heading\n\t\t\tconst envHeadingVisible = await page.getByText(/Окружения|Environments|Environment/)\n\t\t\t\t.first()\n\t\t\t\t.isVisible({ timeout: 15_000 })\n\t\t\t\t.catch(() => false);\n\t\t\tconsole.log(`[E2E] ✅ Stage 1: Settings → Environments tab visible: ${envHeadingVisible}`);\n\t\t});\n\n\t\tawait test.step('Verify via API that environment is persisted', async () => {\n\t\t\tconst envs = await apiGet('/api/settings/environments');\n\t\t\tconst ssDevEnv = envs.find(e => e.id === 'ss-dev');\n\t\t\tif (!ssDevEnv) {\n\t\t\t\tconsole.log('[E2E] ⚠️ Stage 1: Environment ss-dev not found via API, may have failed to create');\n\t\t\t} else {\n\t\t\t\texpect(ssDevEnv).toBeDefined();\n\t\t\t\texpect(ssDevEnv.url).toContain('ss-dev.bebesh.ru');\n\t\t\t\texpect(ssDevEnv.username).toBe('admin');\n\t\t\t\tconsole.log(`[E2E] ✅ Stage 1: API confirms env ss-dev: ${ssDevEnv.url}`);\n\t\t\t}\n\t\t});\n\t});\n\n\t// ── 1.4. DASHBOARD HUB LOADS WITHOUT WIZARD ───────────────\n\ttest('dashboard hub loads normally after environment creation', async ({ authPage }) => {\n\t\tconst page = authPage;\n\t\tawait test.step('Navigate to dashboards', async () => {\n\t\t\t// authPage fixture already logged in; go to dashboards\n\t\t\tawait page.goto(`${FRONTEND_URL}/dashboards`);\n\t\t\tawait page.waitForURL(/\\/dashboards/, { timeout: 10_000 });\n\t\t\tconsole.log('[E2E] ✅ Stage 1: Dashboard hub reached');\n\t\t});\n\n\t\tawait test.step('No wizard appears on subsequent visits', async () => {\n\t\t\t// Verify wizard is NOT present (if environment was created in previous tests)\n\t\t\tconst wizardPresent = await page.getByText(/Configure your first environment|Настройте первое окружение/)\n\t\t\t\t.isVisible({ timeout: 3_000 })\n\t\t\t\t.catch(() => false);\n\t\t\tif (wizardPresent) {\n\t\t\t\tconsole.log('[E2E] ⚠️ Stage 1: Wizard still visible — env was not created or creation failed');\n\t\t\t} else {\n\t\t\t\tconsole.log('[E2E] ✅ Stage 1: No wizard on repeat visit');\n\t\t\t}\n\t\t});\n\n\t\tawait test.step('Environment selector visible in header', async () => {\n\t\t\t// Environment selector should now show the ss-dev environment\n\t\t\tconst envSelectorVisible = await page.getByText(/ss-dev/).first()\n\t\t\t\t.isVisible({ timeout: 5_000 })\n\t\t\t\t.catch(() => false);\n\t\t\tconsole.log(`[E2E] ✅ Stage 1: Environment selector visible: ${envSelectorVisible}`);\n\t\t});\n\t});\n});\n// #endregion EnterpriseCleanSetupE2E\n" }, { "contract_id": "GitE2E", @@ -63589,14 +58767,14 @@ { "source_id": "GitE2E", "relation_type": "BINDS_TO", - "target_id": "[EXT:list:GitDashboardPage_GitConfigRoutes]", - "target_ref": "[[EXT:list:GitDashboardPage_GitConfigRoutes]]" + "target_id": "GitSettingsPage", + "target_ref": "[GitSettingsPage]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region GitE2E [C:3] [TYPE Test] [SEMANTICS e2e, git, integration, config]\n// @BRIEF E2E tests for Git integration — config CRUD, connection test.\n// @RELATION BINDS_TO -> [[EXT:list:GitDashboardPage_GitConfigRoutes]]\n// @UX_STATE ConfigCreated -> Git server appears in configured list.\n// @UX_STATE ConnectionTested -> Success/failure toast feedback.\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiGet, apiPost, apiDelete } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\nconst GITEA_URL = process.env.GITEA_URL || 'https://git.bebesh.ru';\nconst GITEA_TOKEN = process.env.GITEA_TOKEN || 'd5e3a0bea62121dcafbf33070e85d65e8a383c20';\n\ntest.describe('Git Integration', () => {\n\ttest('should have git config visible in settings UI', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\tawait authPage.waitForSelector('text=Интеграция Git', { timeout: 10_000 });\n\n\t\t// Configured servers section visible\n\t\tawait expect(authPage.getByText(/Настроенные серверы/)).toBeVisible();\n\t\t// Add git server form visible\n\t\tawait expect(authPage.getByText(/Добавить Git-сервер|Add Git server/)).toBeVisible();\n\t});\n\n\ttest('should create git config via API and verify in UI', async ({ authPage }) => {\n\t\tconst configName = `E2E-Gitea-${Date.now()}`;\n\n\t\t// Create via API\n\t\tconst config = await apiPost('/api/git/config', {\n\t\t\tname: configName,\n\t\t\tprovider: 'GITEA',\n\t\t\turl: GITEA_URL,\n\t\t\tpat: GITEA_TOKEN,\n\t\t\tdefault_repository: 'busya/ss-tools',\n\t\t\tdefault_branch: 'main',\n\t\t});\n\t\texpect(config).toBeDefined();\n\t\texpect(config.id).toBeTruthy();\n\t\texpect(config.provider).toBe('GITEA');\n\n\t\t// Verify in UI\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\tawait authPage.waitForSelector('text=Интеграция Git', { timeout: 10_000 });\n\t\tawait expect(authPage.getByText(configName).first()).toBeVisible({ timeout: 10_000 });\n\n\t\t// Cleanup\n\t\tawait apiDelete(`/api/git/config/${config.id}`);\n\t});\n\n\ttest('should test git connection successfully', async ({ authPage }) => {\n\t\t// Get existing configs\n\t\tconst configs = await apiGet('/api/git/config');\n\t\tif (configs.length === 0) {\n\t\t\tconsole.log('[E2E] No git configs to test, skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tconst config = configs[0];\n\t\tconst result = await apiPost('/api/git/config/test', {\n\t\t\tname: config.name,\n\t\t\tprovider: config.provider,\n\t\t\turl: config.url,\n\t\t\tpat: GITEA_TOKEN,\n\t\t\tconfig_id: config.id,\n\t\t});\n\t\texpect(result.status).toBe('success');\n\t});\n\n\ttest('should list Gitea repositories', async ({ authPage }) => {\n\t\tconst configs = await apiGet('/api/git/config');\n\t\tif (configs.length === 0) {\n\t\t\tconsole.log('[E2E] No git configs to list repos, skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tconst config = configs[0];\n\t\tif (config.provider !== 'GITEA') {\n\t\t\tconsole.log('[E2E] Provider is not GITEA, skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tconst repos = await apiGet(`/api/git/config/${config.id}/gitea/repos`);\n\t\texpect(Array.isArray(repos)).toBeTruthy();\n\t\texpect(repos.length).toBeGreaterThan(0);\n\t\t// Should include the ss-tools repo\n\t\tconst hasSsTools = repos.some(r => r.full_name?.includes('ss-tools'));\n\t\texpect(hasSsTools).toBeTruthy();\n\t});\n});\n// #endregion GitE2E\n" + "body": "// #region GitE2E [C:3] [TYPE Test] [SEMANTICS e2e, git, integration, config]\n// @BRIEF E2E tests for Git integration — config CRUD, connection test.\n// @RELATION BINDS_TO -> [GitSettingsPage]\n// @UX_STATE ConfigCreated -> Git server appears in configured list.\n// @UX_STATE ConnectionTested -> Success/failure toast feedback.\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiGet, apiPost, apiDelete } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\nconst GITEA_URL = process.env.GITEA_URL || 'https://git.bebesh.ru';\nconst GITEA_TOKEN = process.env.GITEA_TOKEN || 'd5e3a0bea62121dcafbf33070e85d65e8a383c20';\n\ntest.describe('Git Integration', () => {\n\ttest('should have git config visible in settings UI', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\tawait authPage.waitForSelector('text=Интеграция Git', { timeout: 10_000 });\n\n\t\t// Configured servers section visible\n\t\tawait expect(authPage.getByText(/Настроенные серверы/)).toBeVisible();\n\t\t// Add git server form visible\n\t\tawait expect(authPage.getByText(/Добавить Git-сервер|Add Git server/)).toBeVisible();\n\t});\n\n\ttest('should create git config via API and verify in UI', async ({ authPage }) => {\n\t\tconst configName = `E2E-Gitea-${Date.now()}`;\n\n\t\t// Create via API\n\t\tconst config = await apiPost('/api/git/config', {\n\t\t\tname: configName,\n\t\t\tprovider: 'GITEA',\n\t\t\turl: GITEA_URL,\n\t\t\tpat: GITEA_TOKEN,\n\t\t\tdefault_repository: 'busya/ss-tools',\n\t\t\tdefault_branch: 'main',\n\t\t});\n\t\texpect(config).toBeDefined();\n\t\texpect(config.id).toBeTruthy();\n\t\texpect(config.provider).toBe('GITEA');\n\n\t\t// Verify in UI\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\tawait authPage.waitForSelector('text=Интеграция Git', { timeout: 10_000 });\n\t\tawait expect(authPage.getByText(configName).first()).toBeVisible({ timeout: 10_000 });\n\n\t\t// Cleanup\n\t\tawait apiDelete(`/api/git/config/${config.id}`);\n\t});\n\n\ttest('should test git connection successfully', async ({ authPage }) => {\n\t\t// Get existing configs\n\t\tconst configs = await apiGet('/api/git/config');\n\t\tif (configs.length === 0) {\n\t\t\tconsole.log('[E2E] No git configs to test, skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tconst config = configs[0];\n\t\tconst result = await apiPost('/api/git/config/test', {\n\t\t\tname: config.name,\n\t\t\tprovider: config.provider,\n\t\t\turl: config.url,\n\t\t\tpat: GITEA_TOKEN,\n\t\t\tconfig_id: config.id,\n\t\t});\n\t\texpect(result.status).toBe('success');\n\t});\n\n\ttest('should list Gitea repositories', async ({ authPage }) => {\n\t\tconst configs = await apiGet('/api/git/config');\n\t\tif (configs.length === 0) {\n\t\t\tconsole.log('[E2E] No git configs to list repos, skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tconst config = configs[0];\n\t\tif (config.provider !== 'GITEA') {\n\t\t\tconsole.log('[E2E] Provider is not GITEA, skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tconst repos = await apiGet(`/api/git/config/${config.id}/gitea/repos`);\n\t\texpect(Array.isArray(repos)).toBeTruthy();\n\t\texpect(repos.length).toBeGreaterThan(0);\n\t\t// Should include the ss-tools repo\n\t\tconst hasSsTools = repos.some(r => r.full_name?.includes('ss-tools'));\n\t\texpect(hasSsTools).toBeTruthy();\n\t});\n});\n// #endregion GitE2E\n" }, { "contract_id": "LiveProjectCheckE2E", @@ -63614,7 +58792,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region LiveProjectCheckE2E [C:4] [TYPE Test] [SEMANTICS e2e, live-project, verification, dashboard, llm, settings]\n// @BRIEF Stage 2: E2E test for live project verification — validates dashboard LLM analysis,\n// settings modification, and overall project health after ./run.sh startup.\n// @RELATION BINDS_TO -> [[EXT:list:DashboardHub_LLM_SettingsPage]]\n// @RELATION DEPENDS_ON -> [ApiHelper]\n// @UX_STATE DashboardsLoaded -> Dashboard hub with environment context, dashboard cards visible.\n// @UX_STATE LLMAnalysis -> LLM analysis triggered and report generated for a dashboard.\n// @UX_STATE SettingsChanged -> Setting value modified and persisted across reload.\n// @RATIONALE After ./run.sh launches the local dev stack, the project should be fully operational.\n// This test verifies the critical paths: login, dashboard rendering with LLM analysis,\n// and settings persistence — ensuring dev stack is healthy before image delivery.\n// @REJECTED Testing individual API endpoints in isolation — must test full UI flow for delivery sign-off.\n// @INVARIANT LLM provider must be configured for LLM analysis to work.\n// @INVARIANT Settings changes persist after page reload.\n//\n// Usage:\n// Start dev stack: ./run.sh\n// Then run: npx playwright test --grep \"Live Project Check\"\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiGet, apiPost } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\nconst BACKEND_URL = process.env.BACKEND_URL || 'http://127.0.0.1:8101';\n\ntest.describe('Live Project Check — Stage 2: Verification', () => {\n\n\t// ── 2.1. LOGIN ───────────────────────────────────────────\n\ttest('login to live project with admin/admin123', async ({ page }) => {\n\t\tawait test.step('Navigate to login page', async () => {\n\t\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\t\t\tawait page.waitForSelector('form', { timeout: 15_000 });\n\t\t\tawait expect(page.getByRole('heading', { name: /Вход|Login/ })).toBeVisible();\n\t\t\tconsole.log('[E2E] ✅ Stage 2: Login page loaded');\n\t\t});\n\n\t\tawait test.step('Submit credentials', async () => {\n\t\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\t\tawait page.getByLabel(/Пароль|Password/).fill('admin123');\n\t\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\t\t\tawait page.waitForURL(/\\/(?!login)/, { timeout: 15_000 });\n\t\t\t// Use .first() to avoid strict mode violation with multiple