diff --git a/.axiom/semantic_index/index.json b/.axiom/semantic_index/index.json index 4f323e7d..ec483b9f 100644 --- a/.axiom/semantic_index/index.json +++ b/.axiom/semantic_index/index.json @@ -1,20 +1,21 @@ { - "generated_at": "2026-05-17T09:53:01.679154203Z", + "generated_at": "2026-05-19T15:29:39.447872053Z", "root_path": "/home/busya/dev/ss-tools", - "contract_count": 2483, - "edge_count": 1759, - "file_count": 535, + "contract_count": 2620, + "edge_count": 1952, + "file_count": 632, "contracts": [ { "contract_id": "DeleteRunningTasksUtil", "contract_type": "Module", "file_path": "backend/delete_running_tasks.py", "start_line": 2, - "end_line": 48, - "tier": "TIER_1", - "complexity": 1, + "end_line": 50, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "LAYER": "Utility", + "COMPLEXITY": 3, + "LAYER": "Infrastructure", "PURPOSE": "Script to delete tasks with RUNNING status from the database.", "SEMANTICS": [ "maintenance", @@ -26,8 +27,8 @@ { "source_id": "DeleteRunningTasksUtil", "relation_type": "DEPENDS_ON", - "target_id": "TasksSessionLocal", - "target_ref": "[TasksSessionLocal]" + "target_id": "TaskRecord", + "target_ref": "[TaskRecord]" }, { "source_id": "DeleteRunningTasksUtil", @@ -36,95 +37,41 @@ "target_ref": "[TaskRecord]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Utility' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Utility" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "def_format_deprecated", - "tag": "FORMAT", - "message": "Contract 'DeleteRunningTasksUtil' uses legacy [DEF:] format. Consider migrating to #region/#endregion for new code. Use axiom_contract_refactor with operation='migrate_def_to_region' to auto-convert.", - "detail": { - "current_format": "def", - "migration_hint": "Use axiom_contract_refactor operation=migrate_def_to_region", - "recommended_format": "region" - } - } - ], - "anchor_syntax": "def", + "schema_warnings": [], + "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# [DEF:DeleteRunningTasksUtil:Module]\n# @PURPOSE: Script to delete tasks with RUNNING status from the database.\n# @LAYER: Utility\n# @SEMANTICS: maintenance, database, cleanup\n# @RELATION: DEPENDS_ON ->[TasksSessionLocal]\n# @RELATION: DEPENDS_ON ->[TaskRecord]\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import TasksSessionLocal\nfrom src.models.task import TaskRecord\n\n\n# [DEF:delete_running_tasks:Function]\n# @PURPOSE: Delete all tasks with RUNNING status from the database.\n# @PRE: Database is accessible and TaskRecord model is defined.\n# @POST: All tasks with status 'RUNNING' are removed from the database.\ndef delete_running_tasks():\n \"\"\"Delete all tasks with RUNNING status from the database.\"\"\"\n session: Session = TasksSessionLocal()\n try:\n # Find all task records with RUNNING status\n running_tasks = session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").all()\n\n if not running_tasks:\n print(\"No RUNNING tasks found.\")\n return\n\n print(f\"Found {len(running_tasks)} RUNNING tasks:\")\n for task in running_tasks:\n print(f\"- Task ID: {task.id}, Type: {task.type}\")\n\n # Delete the found tasks\n session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").delete(synchronize_session=False)\n session.commit()\n\n print(f\"Successfully deleted {len(running_tasks)} RUNNING tasks.\")\n except Exception as e:\n session.rollback()\n print(f\"Error deleting tasks: {e}\")\n finally:\n session.close()\n# [/DEF:delete_running_tasks:Function]\n\nif __name__ == \"__main__\":\n delete_running_tasks()\n# [/DEF:DeleteRunningTasksUtil:Module]\n" + "body": "# #region DeleteRunningTasksUtil [C:3] [TYPE Module]\n# @PURPOSE Script to delete tasks with RUNNING status from the database.\n# @LAYER Infrastructure\n# @SEMANTICS maintenance, database, cleanup\n# @RELATION DEPENDS_ON ->[TaskRecord]\n# @RELATION DEPENDS_ON ->[TaskRecord]\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import TasksSessionLocal\nfrom src.models.task import TaskRecord\n\n# #region delete_running_tasks [C:4] [TYPE Function]\n# @PURPOSE Delete all tasks with RUNNING status from the database.\n# @PRE Database is accessible and TaskRecord model is defined.\n# @POST All tasks with status 'RUNNING' are removed from the database.\n# @SIDE_EFFECT Modifies DB: deletes RUNNING tasks. Prints to stdout.\n\ndef delete_running_tasks():\n \"\"\"Delete all tasks with RUNNING status from the database.\"\"\"\n session: Session = TasksSessionLocal()\n try:\n # Find all task records with RUNNING status\n running_tasks = session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").all()\n\n if not running_tasks:\n print(\"No RUNNING tasks found.\")\n return\n\n print(f\"Found {len(running_tasks)} RUNNING tasks:\")\n for task in running_tasks:\n print(f\"- Task ID: {task.id}, Type: {task.type}\")\n\n # Delete the found tasks\n session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").delete(synchronize_session=False)\n session.commit()\n\n print(f\"Successfully deleted {len(running_tasks)} RUNNING tasks.\")\n except Exception as e:\n session.rollback()\n print(f\"Error deleting tasks: {e}\")\n finally:\n session.close()\n# #endregion delete_running_tasks\n# [/DEF:delete_running_tasks:Function]\n\nif __name__ == \"__main__\":\n delete_running_tasks()\n# #endregion DeleteRunningTasksUtil\n" }, { "contract_id": "delete_running_tasks", "contract_type": "Function", "file_path": "backend/delete_running_tasks.py", - "start_line": 15, - "end_line": 44, - "tier": "TIER_1", - "complexity": 1, + "start_line": 14, + "end_line": 45, + "tier": "TIER_2", + "complexity": 4, "metadata": { + "COMPLEXITY": 4, "POST": "All tasks with status 'RUNNING' are removed from the database.", "PRE": "Database is accessible and TaskRecord model is defined.", - "PURPOSE": "Delete all tasks with RUNNING status from the database." + "PURPOSE": "Delete all tasks with RUNNING status from the database.", + "SIDE_EFFECT": "Modifies DB: deletes RUNNING tasks. Prints to stdout." }, "relations": [], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 1, + "actual_complexity": 4, "contract_type": "Function" } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "def_format_deprecated", - "tag": "FORMAT", - "message": "Contract 'delete_running_tasks' uses legacy [DEF:] format. Consider migrating to #region/#endregion for new code. Use axiom_contract_refactor with operation='migrate_def_to_region' to auto-convert.", - "detail": { - "current_format": "def", - "migration_hint": "Use axiom_contract_refactor operation=migrate_def_to_region", - "recommended_format": "region" - } } ], - "anchor_syntax": "def", + "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# [DEF:delete_running_tasks:Function]\n# @PURPOSE: Delete all tasks with RUNNING status from the database.\n# @PRE: Database is accessible and TaskRecord model is defined.\n# @POST: All tasks with status 'RUNNING' are removed from the database.\ndef delete_running_tasks():\n \"\"\"Delete all tasks with RUNNING status from the database.\"\"\"\n session: Session = TasksSessionLocal()\n try:\n # Find all task records with RUNNING status\n running_tasks = session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").all()\n\n if not running_tasks:\n print(\"No RUNNING tasks found.\")\n return\n\n print(f\"Found {len(running_tasks)} RUNNING tasks:\")\n for task in running_tasks:\n print(f\"- Task ID: {task.id}, Type: {task.type}\")\n\n # Delete the found tasks\n session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").delete(synchronize_session=False)\n session.commit()\n\n print(f\"Successfully deleted {len(running_tasks)} RUNNING tasks.\")\n except Exception as e:\n session.rollback()\n print(f\"Error deleting tasks: {e}\")\n finally:\n session.close()\n# [/DEF:delete_running_tasks:Function]\n" + "body": "# #region delete_running_tasks [C:4] [TYPE Function]\n# @PURPOSE Delete all tasks with RUNNING status from the database.\n# @PRE Database is accessible and TaskRecord model is defined.\n# @POST All tasks with status 'RUNNING' are removed from the database.\n# @SIDE_EFFECT Modifies DB: deletes RUNNING tasks. Prints to stdout.\n\ndef delete_running_tasks():\n \"\"\"Delete all tasks with RUNNING status from the database.\"\"\"\n session: Session = TasksSessionLocal()\n try:\n # Find all task records with RUNNING status\n running_tasks = session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").all()\n\n if not running_tasks:\n print(\"No RUNNING tasks found.\")\n return\n\n print(f\"Found {len(running_tasks)} RUNNING tasks:\")\n for task in running_tasks:\n print(f\"- Task ID: {task.id}, Type: {task.type}\")\n\n # Delete the found tasks\n session.query(TaskRecord).filter(TaskRecord.status == \"RUNNING\").delete(synchronize_session=False)\n session.commit()\n\n print(f\"Successfully deleted {len(running_tasks)} RUNNING tasks.\")\n except Exception as e:\n session.rollback()\n print(f\"Error deleting tasks: {e}\")\n finally:\n session.close()\n# #endregion delete_running_tasks\n" }, { "contract_id": "SrcRoot", @@ -193,33 +140,37 @@ "contract_type": "Module", "file_path": "backend/src/api/auth.py", "start_line": 1, - "end_line": 141, - "tier": "TIER_2", - "complexity": 3, + "end_line": 154, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Input -> OAuth2PasswordRequestForm -> Token, User", "INVARIANT": "All auth endpoints must return consistent error codes.", "LAYER": "API", - "PURPOSE": "Authentication API endpoints." + "POST": "FastAPI app instance with auth routes registered.", + "PRE": "Python environment and dependencies installed; database available.", + "PURPOSE": "Authentication API endpoints.", + "SIDE_EFFECT": "Registers API routes; configures OAuth and CORS middleware." }, "relations": [ { "source_id": "AuthApi", "relation_type": "DEPENDS_ON", - "target_id": "AuthService", - "target_ref": "[AuthService]" + "target_id": "is_adfs_configured", + "target_ref": "[is_adfs_configured]" }, { "source_id": "AuthApi", "relation_type": "DEPENDS_ON", - "target_id": "get_auth_db", - "target_ref": "[get_auth_db]" + "target_id": "is_adfs_configured", + "target_ref": "[is_adfs_configured]" }, { "source_id": "AuthApi", "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" + "target_id": "is_adfs_configured", + "target_ref": "[is_adfs_configured]" }, { "source_id": "AuthApi", @@ -228,41 +179,32 @@ "target_ref": "[is_adfs_configured]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthApi [C:3] [TYPE Module] [SEMANTICS fastapi, auth, api]\n#\n# @BRIEF Authentication API endpoints.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AuthService]\n# @RELATION DEPENDS_ON -> [get_auth_db]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @INVARIANT: All auth endpoints must return consistent error codes.\n\nimport starlette.requests\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.logger import log_security_event\nfrom ..core.auth.oauth import is_adfs_configured, oauth\nfrom ..core.database import get_auth_db\nfrom ..core.logger import belief_scope\nfrom ..dependencies import get_current_user\nfrom ..schemas.auth import Token\nfrom ..schemas.auth import User as UserSchema\nfrom ..services.auth_service import AuthService\n\n# #region router [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> [fastapi.APIRouter]\n# @BRIEF APIRouter instance for authentication routes.\nrouter = APIRouter(prefix=\"/api/auth\", tags=[\"auth\"])\n# #endregion router\n\n\n# #region login_for_access_token [C:3] [TYPE Function]\n# @BRIEF Authenticates a user and returns a JWT access token.\n# @PRE: form_data contains username and password.\n# @POST: Returns a Token object on success.\n# @RELATION CALLS -> [AuthService.authenticate_user]\n# @RELATION CALLS -> [AuthService.create_session]\n@router.post(\"/login\", response_model=Token)\nasync def login_for_access_token(\n form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.login\"):\n auth_service = AuthService(db)\n user = auth_service.authenticate_user(form_data.username, form_data.password)\n if not user:\n log_security_event(\n \"LOGIN_FAILED\", form_data.username, {\"reason\": \"Invalid credentials\"}\n )\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Incorrect username or password\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n log_security_event(\"LOGIN_SUCCESS\", user.username, {\"source\": \"LOCAL\"})\n return auth_service.create_session(user)\n\n\n# #endregion login_for_access_token\n\n\n# #region read_users_me [C:3] [TYPE Function]\n# @BRIEF Retrieves the profile of the currently authenticated user.\n# @PRE: Valid JWT token provided.\n# @POST: Returns the current user's data.\n# @RELATION DEPENDS_ON -> [get_current_user]\n@router.get(\"/me\", response_model=UserSchema)\nasync def read_users_me(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.me\"):\n return current_user\n\n\n# #endregion read_users_me\n\n\n# #region logout [C:3] [TYPE Function]\n# @BRIEF Logs out the current user (placeholder for session revocation).\n# @PRE: Valid JWT token provided.\n# @POST: Returns success message.\n# @RELATION DEPENDS_ON -> [get_current_user]\n@router.post(\"/logout\")\nasync def logout(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.logout\"):\n log_security_event(\"LOGOUT\", current_user.username)\n # In a stateless JWT setup, client-side token deletion is primary.\n # Server-side revocation (blacklisting) can be added here if needed.\n return {\"message\": \"Successfully logged out\"}\n\n\n# #endregion logout\n\n\n# #region login_adfs [C:3] [TYPE Function]\n# @BRIEF Initiates the ADFS OIDC login flow.\n# @POST: Redirects the user to ADFS.\n# @RELATION USES -> [is_adfs_configured]\n@router.get(\"/login/adfs\")\nasync def login_adfs(request: starlette.requests.Request):\n with belief_scope(\"api.auth.login_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n redirect_uri = request.url_for(\"auth_callback_adfs\")\n return await oauth.adfs.authorize_redirect(request, str(redirect_uri))\n\n\n# #endregion login_adfs\n\n\n# #region auth_callback_adfs [C:3] [TYPE Function]\n# @BRIEF Handles the callback from ADFS after successful authentication.\n# @POST: Provisions user JIT and returns session token.\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @RELATION CALLS -> [AuthService.provision_adfs_user]\n# @RELATION CALLS -> [AuthService.create_session]\n@router.get(\"/callback/adfs\", name=\"auth_callback_adfs\")\nasync def auth_callback_adfs(\n request: starlette.requests.Request, db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.callback_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n token = await oauth.adfs.authorize_access_token(request)\n user_info = token.get(\"userinfo\")\n if not user_info:\n raise HTTPException(\n status_code=400, detail=\"Failed to retrieve user info from ADFS\"\n )\n\n auth_service = AuthService(db)\n user = auth_service.provision_adfs_user(user_info)\n return auth_service.create_session(user)\n\n\n# #endregion auth_callback_adfs\n\n# #endregion AuthApi\n" + "body": "# #region AuthApi [C:5] [TYPE Module] [SEMANTICS fastapi, auth, api]\n# @BRIEF Authentication API endpoints.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @PRE Python environment and dependencies installed; database available.\n# @POST FastAPI app instance with auth routes registered.\n# @SIDE_EFFECT Registers API routes; configures OAuth and CORS middleware.\n# @DATA_CONTRACT Input -> OAuth2PasswordRequestForm -> Token, User\n# @INVARIANT All auth endpoints must return consistent error codes.\n\nimport starlette.requests\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.logger import log_security_event\nfrom ..core.auth.oauth import is_adfs_configured, oauth\nfrom ..core.database import get_auth_db\nfrom ..core.logger import belief_scope\nfrom ..dependencies import get_current_user\nfrom ..schemas.auth import Token\nfrom ..schemas.auth import User as UserSchema\nfrom ..services.auth_service import AuthService\n\n# #region router [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> [fastapi.APIRouter]\n# @BRIEF APIRouter instance for authentication routes.\nrouter = APIRouter(prefix=\"/api/auth\", tags=[\"auth\"])\n# #endregion router\n\n\n# #region login_for_access_token [C:4] [TYPE Function]\n# @BRIEF Authenticates a user and returns a JWT access token.\n# @PRE form_data contains username and password.\n# @POST Returns a Token object on success.\n# @RELATION CALLS -> [AuthService.create_session]\n# @RELATION CALLS -> [AuthService.create_session]\n# @SIDE_EFFECT DB read/write for auth session; writes security event log.\n\n@router.post(\"/login\", response_model=Token)\nasync def login_for_access_token(\n form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.login\"):\n auth_service = AuthService(db)\n user = auth_service.authenticate_user(form_data.username, form_data.password)\n if not user:\n log_security_event(\n \"LOGIN_FAILED\", form_data.username, {\"reason\": \"Invalid credentials\"}\n )\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Incorrect username or password\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n log_security_event(\"LOGIN_SUCCESS\", user.username, {\"source\": \"LOCAL\"})\n return auth_service.create_session(user)\n\n\n# #endregion login_for_access_token\n\n\n# #region read_users_me [C:4] [TYPE Function]\n# @BRIEF Retrieves the profile of the currently authenticated user.\n# @PRE Valid JWT token provided.\n# @POST Returns the current user's data.\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @SIDE_EFFECT Reads current user from DB via auth middleware; writes security event log.\n\n@router.get(\"/me\", response_model=UserSchema)\nasync def read_users_me(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.me\"):\n return current_user\n\n\n# #endregion read_users_me\n\n\n# #region logout [C:4] [TYPE Function]\n# @BRIEF Logs out the current user (placeholder for session revocation).\n# @PRE Valid JWT token provided.\n# @POST Returns success message.\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @SIDE_EFFECT Writes security event LOGOUT event.\n\n@router.post(\"/logout\")\nasync def logout(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.logout\"):\n log_security_event(\"LOGOUT\", current_user.username)\n # In a stateless JWT setup, client-side token deletion is primary.\n # Server-side revocation (blacklisting) can be added here if needed.\n return {\"message\": \"Successfully logged out\"}\n\n\n# #endregion logout\n\n\n# #region login_adfs [C:4] [TYPE Function]\n# @BRIEF Initiates the ADFS OIDC login flow.\n# @POST Redirects the user to ADFS.\n# @RELATION USES -> [is_adfs_configured]\n# @SIDE_EFFECT Redirects user to ADFS external OIDC provider.\n\n@router.get(\"/login/adfs\")\nasync def login_adfs(request: starlette.requests.Request):\n with belief_scope(\"api.auth.login_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n redirect_uri = request.url_for(\"auth_callback_adfs\")\n return await oauth.adfs.authorize_redirect(request, str(redirect_uri))\n\n\n# #endregion login_adfs\n\n\n# #region auth_callback_adfs [C:4] [TYPE Function]\n# @BRIEF Handles the callback from ADFS after successful authentication.\n# @POST Provisions user JIT and returns session token.\n# @RELATION CALLS -> [AuthService.create_session]\n# @RELATION CALLS -> [AuthService.create_session]\n# @RELATION CALLS -> [AuthService.create_session]\n# @SIDE_EFFECT Provisions user in DB, creates auth session, writes security event log.\n\n@router.get(\"/callback/adfs\", name=\"auth_callback_adfs\")\nasync def auth_callback_adfs(\n request: starlette.requests.Request, db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.callback_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n token = await oauth.adfs.authorize_access_token(request)\n user_info = token.get(\"userinfo\")\n if not user_info:\n raise HTTPException(\n status_code=400, detail=\"Failed to retrieve user info from ADFS\"\n )\n\n auth_service = AuthService(db)\n user = auth_service.provision_adfs_user(user_info)\n return auth_service.create_session(user)\n\n\n# #endregion auth_callback_adfs\n\n# #endregion AuthApi\n" }, { "contract_id": "login_for_access_token", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 32, - "end_line": 58, + "start_line": 35, + "end_line": 63, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Returns a Token object on success.", "PRE": "form_data contains username and password.", - "PURPOSE": "Authenticates a user and returns a JWT access token." + "PURPOSE": "Authenticates a user and returns a JWT access token.", + "SIDE_EFFECT": "DB read/write for auth session; writes security event log." }, "relations": [ { "source_id": "login_for_access_token", "relation_type": "CALLS", - "target_id": "AuthService.authenticate_user", - "target_ref": "[AuthService.authenticate_user]" + "target_id": "AuthService.create_session", + "target_ref": "[AuthService.create_session]" }, { "source_id": "login_for_access_token", @@ -271,43 +213,25 @@ "target_ref": "[AuthService.create_session]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region login_for_access_token [C:3] [TYPE Function]\n# @BRIEF Authenticates a user and returns a JWT access token.\n# @PRE: form_data contains username and password.\n# @POST: Returns a Token object on success.\n# @RELATION CALLS -> [AuthService.authenticate_user]\n# @RELATION CALLS -> [AuthService.create_session]\n@router.post(\"/login\", response_model=Token)\nasync def login_for_access_token(\n form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.login\"):\n auth_service = AuthService(db)\n user = auth_service.authenticate_user(form_data.username, form_data.password)\n if not user:\n log_security_event(\n \"LOGIN_FAILED\", form_data.username, {\"reason\": \"Invalid credentials\"}\n )\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Incorrect username or password\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n log_security_event(\"LOGIN_SUCCESS\", user.username, {\"source\": \"LOCAL\"})\n return auth_service.create_session(user)\n\n\n# #endregion login_for_access_token\n" + "body": "# #region login_for_access_token [C:4] [TYPE Function]\n# @BRIEF Authenticates a user and returns a JWT access token.\n# @PRE form_data contains username and password.\n# @POST Returns a Token object on success.\n# @RELATION CALLS -> [AuthService.create_session]\n# @RELATION CALLS -> [AuthService.create_session]\n# @SIDE_EFFECT DB read/write for auth session; writes security event log.\n\n@router.post(\"/login\", response_model=Token)\nasync def login_for_access_token(\n form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.login\"):\n auth_service = AuthService(db)\n user = auth_service.authenticate_user(form_data.username, form_data.password)\n if not user:\n log_security_event(\n \"LOGIN_FAILED\", form_data.username, {\"reason\": \"Invalid credentials\"}\n )\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Incorrect username or password\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n log_security_event(\"LOGIN_SUCCESS\", user.username, {\"source\": \"LOCAL\"})\n return auth_service.create_session(user)\n\n\n# #endregion login_for_access_token\n" }, { "contract_id": "read_users_me", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 61, - "end_line": 72, + "start_line": 66, + "end_line": 79, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Returns the current user's data.", "PRE": "Valid JWT token provided.", - "PURPOSE": "Retrieves the profile of the currently authenticated user." + "PURPOSE": "Retrieves the profile of the currently authenticated user.", + "SIDE_EFFECT": "Reads current user from DB via auth middleware; writes security event log." }, "relations": [ { @@ -317,43 +241,25 @@ "target_ref": "[get_current_user]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region read_users_me [C:3] [TYPE Function]\n# @BRIEF Retrieves the profile of the currently authenticated user.\n# @PRE: Valid JWT token provided.\n# @POST: Returns the current user's data.\n# @RELATION DEPENDS_ON -> [get_current_user]\n@router.get(\"/me\", response_model=UserSchema)\nasync def read_users_me(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.me\"):\n return current_user\n\n\n# #endregion read_users_me\n" + "body": "# #region read_users_me [C:4] [TYPE Function]\n# @BRIEF Retrieves the profile of the currently authenticated user.\n# @PRE Valid JWT token provided.\n# @POST Returns the current user's data.\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @SIDE_EFFECT Reads current user from DB via auth middleware; writes security event log.\n\n@router.get(\"/me\", response_model=UserSchema)\nasync def read_users_me(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.me\"):\n return current_user\n\n\n# #endregion read_users_me\n" }, { "contract_id": "logout", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 75, - "end_line": 89, + "start_line": 82, + "end_line": 98, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Returns success message.", "PRE": "Valid JWT token provided.", - "PURPOSE": "Logs out the current user (placeholder for session revocation)." + "PURPOSE": "Logs out the current user (placeholder for session revocation).", + "SIDE_EFFECT": "Writes security event LOGOUT event." }, "relations": [ { @@ -363,42 +269,24 @@ "target_ref": "[get_current_user]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region logout [C:3] [TYPE Function]\n# @BRIEF Logs out the current user (placeholder for session revocation).\n# @PRE: Valid JWT token provided.\n# @POST: Returns success message.\n# @RELATION DEPENDS_ON -> [get_current_user]\n@router.post(\"/logout\")\nasync def logout(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.logout\"):\n log_security_event(\"LOGOUT\", current_user.username)\n # In a stateless JWT setup, client-side token deletion is primary.\n # Server-side revocation (blacklisting) can be added here if needed.\n return {\"message\": \"Successfully logged out\"}\n\n\n# #endregion logout\n" + "body": "# #region logout [C:4] [TYPE Function]\n# @BRIEF Logs out the current user (placeholder for session revocation).\n# @PRE Valid JWT token provided.\n# @POST Returns success message.\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @SIDE_EFFECT Writes security event LOGOUT event.\n\n@router.post(\"/logout\")\nasync def logout(current_user: UserSchema = Depends(get_current_user)):\n with belief_scope(\"api.auth.logout\"):\n log_security_event(\"LOGOUT\", current_user.username)\n # In a stateless JWT setup, client-side token deletion is primary.\n # Server-side revocation (blacklisting) can be added here if needed.\n return {\"message\": \"Successfully logged out\"}\n\n\n# #endregion logout\n" }, { "contract_id": "login_adfs", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 92, - "end_line": 108, + "start_line": 101, + "end_line": 119, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Redirects the user to ADFS.", - "PURPOSE": "Initiates the ADFS OIDC login flow." + "PURPOSE": "Initiates the ADFS OIDC login flow.", + "SIDE_EFFECT": "Redirects user to ADFS external OIDC provider." }, "relations": [ { @@ -410,11 +298,11 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } }, @@ -439,33 +327,34 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region login_adfs [C:3] [TYPE Function]\n# @BRIEF Initiates the ADFS OIDC login flow.\n# @POST: Redirects the user to ADFS.\n# @RELATION USES -> [is_adfs_configured]\n@router.get(\"/login/adfs\")\nasync def login_adfs(request: starlette.requests.Request):\n with belief_scope(\"api.auth.login_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n redirect_uri = request.url_for(\"auth_callback_adfs\")\n return await oauth.adfs.authorize_redirect(request, str(redirect_uri))\n\n\n# #endregion login_adfs\n" + "body": "# #region login_adfs [C:4] [TYPE Function]\n# @BRIEF Initiates the ADFS OIDC login flow.\n# @POST Redirects the user to ADFS.\n# @RELATION USES -> [is_adfs_configured]\n# @SIDE_EFFECT Redirects user to ADFS external OIDC provider.\n\n@router.get(\"/login/adfs\")\nasync def login_adfs(request: starlette.requests.Request):\n with belief_scope(\"api.auth.login_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n redirect_uri = request.url_for(\"auth_callback_adfs\")\n return await oauth.adfs.authorize_redirect(request, str(redirect_uri))\n\n\n# #endregion login_adfs\n" }, { "contract_id": "auth_callback_adfs", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 111, - "end_line": 139, + "start_line": 122, + "end_line": 152, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Provisions user JIT and returns session token.", - "PURPOSE": "Handles the callback from ADFS after successful authentication." + "PURPOSE": "Handles the callback from ADFS after successful authentication.", + "SIDE_EFFECT": "Provisions user in DB, creates auth session, writes security event log." }, "relations": [ { "source_id": "auth_callback_adfs", - "relation_type": "DEPENDS_ON", - "target_id": "is_adfs_configured", - "target_ref": "[is_adfs_configured]" + "relation_type": "CALLS", + "target_id": "AuthService.create_session", + "target_ref": "[AuthService.create_session]" }, { "source_id": "auth_callback_adfs", "relation_type": "CALLS", - "target_id": "AuthService.provision_adfs_user", - "target_ref": "[AuthService.provision_adfs_user]" + "target_id": "AuthService.create_session", + "target_ref": "[AuthService.create_session]" }, { "source_id": "auth_callback_adfs", @@ -476,31 +365,33 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region auth_callback_adfs [C:3] [TYPE Function]\n# @BRIEF Handles the callback from ADFS after successful authentication.\n# @POST: Provisions user JIT and returns session token.\n# @RELATION DEPENDS_ON -> [is_adfs_configured]\n# @RELATION CALLS -> [AuthService.provision_adfs_user]\n# @RELATION CALLS -> [AuthService.create_session]\n@router.get(\"/callback/adfs\", name=\"auth_callback_adfs\")\nasync def auth_callback_adfs(\n request: starlette.requests.Request, db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.callback_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n token = await oauth.adfs.authorize_access_token(request)\n user_info = token.get(\"userinfo\")\n if not user_info:\n raise HTTPException(\n status_code=400, detail=\"Failed to retrieve user info from ADFS\"\n )\n\n auth_service = AuthService(db)\n user = auth_service.provision_adfs_user(user_info)\n return auth_service.create_session(user)\n\n\n# #endregion auth_callback_adfs\n" + "body": "# #region auth_callback_adfs [C:4] [TYPE Function]\n# @BRIEF Handles the callback from ADFS after successful authentication.\n# @POST Provisions user JIT and returns session token.\n# @RELATION CALLS -> [AuthService.create_session]\n# @RELATION CALLS -> [AuthService.create_session]\n# @RELATION CALLS -> [AuthService.create_session]\n# @SIDE_EFFECT Provisions user in DB, creates auth session, writes security event log.\n\n@router.get(\"/callback/adfs\", name=\"auth_callback_adfs\")\nasync def auth_callback_adfs(\n request: starlette.requests.Request, db: Session = Depends(get_auth_db)\n):\n with belief_scope(\"api.auth.callback_adfs\"):\n if not is_adfs_configured():\n raise HTTPException(\n status_code=status.HTTP_503_SERVICE_UNAVAILABLE,\n detail=\"ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables.\",\n )\n token = await oauth.adfs.authorize_access_token(request)\n user_info = token.get(\"userinfo\")\n if not user_info:\n raise HTTPException(\n status_code=400, detail=\"Failed to retrieve user info from ADFS\"\n )\n\n auth_service = AuthService(db)\n user = auth_service.provision_adfs_user(user_info)\n return auth_service.create_session(user)\n\n\n# #endregion auth_callback_adfs\n" }, { "contract_id": "ApiRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/__init__.py", "start_line": 1, - "end_line": 56, - "tier": "TIER_2", - "complexity": 3, + "end_line": 60, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "INVARIANT": "Only names listed in __all__ are importable via __getattr__.", "LAYER": "API", + "POST": "Route modules are lazily loadable via __getattr__", + "PRE": "FastAPI app initialized, route modules available in package", "PURPOSE": "Provide lazy route module loading to avoid heavyweight imports during tests." }, "relations": [ @@ -519,30 +410,41 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ApiRoutesModule [C:3] [TYPE Module] [SEMANTICS api, package, router, lazy, import]\n# @BRIEF Provide lazy route module loading to avoid heavyweight imports during tests.\n# @LAYER: API\n# @RELATION CALLS -> [ApiRoutesGetAttr]\n# @RELATION BINDS_TO -> [Route_Group_Contracts]\n# @INVARIANT: Only names listed in __all__ are importable via __getattr__.\n\n# #region Route_Group_Contracts [C:3] [TYPE Block]\n# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion.\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ConnectionsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n__all__ = [\n \"admin\",\n \"assistant\",\n \"clean_release\",\n \"clean_release_v2\",\n \"connections\",\n \"dashboards\",\n \"dataset_review\",\n \"datasets\",\n \"environments\",\n \"git\",\n \"health\",\n \"llm\",\n \"mappings\",\n \"migration\",\n \"plugins\",\n \"profile\",\n \"reports\",\n \"settings\",\n \"storage\",\n \"tasks\",\n \"translate\",\n]\n# #endregion Route_Group_Contracts\n\n\n# #region ApiRoutesGetAttr [C:3] [TYPE Function]\n# @BRIEF Lazily import route module by attribute name.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @PRE: name is module candidate exposed in __all__.\n# @POST: Returns imported submodule or raises AttributeError.\ndef __getattr__(name):\n if name in __all__:\n import importlib\n\n return importlib.import_module(f\".{name}\", __name__)\n raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n\n\n# #endregion ApiRoutesGetAttr\n# #endregion ApiRoutesModule\n" + "body": "# #region ApiRoutesModule [C:5] [TYPE Module] [SEMANTICS api, package, router, lazy, import]\n# @BRIEF Provide lazy route module loading to avoid heavyweight imports during tests.\n# @LAYER: API\n# @RELATION CALLS -> [ApiRoutesGetAttr]\n# @RELATION BINDS_TO -> [Route_Group_Contracts]\n# @PRE: FastAPI app initialized, route modules available in package\n# @POST: Route modules are lazily loadable via __getattr__\n# @INVARIANT: Only names listed in __all__ are importable via __getattr__.\n\n# #region Route_Group_Contracts [C:3] [TYPE Block]\n# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion.\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ConnectionsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n# @SIDE_EFFECT: Registers route group imports via __getattr__\n# @DATA_CONTRACT: Package -> RouterModule mapping\n__all__ = [\n \"admin\",\n \"assistant\",\n \"clean_release\",\n \"clean_release_v2\",\n \"connections\",\n \"dashboards\",\n \"dataset_review\",\n \"datasets\",\n \"environments\",\n \"git\",\n \"health\",\n \"llm\",\n \"mappings\",\n \"migration\",\n \"plugins\",\n \"profile\",\n \"reports\",\n \"settings\",\n \"storage\",\n \"tasks\",\n \"translate\",\n]\n# #endregion Route_Group_Contracts\n\n\n# #region ApiRoutesGetAttr [C:3] [TYPE Function]\n# @BRIEF Lazily import route module by attribute name.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @PRE: name is module candidate exposed in __all__.\n# @POST: Returns imported submodule or raises AttributeError.\ndef __getattr__(name):\n if name in __all__:\n import importlib\n\n return importlib.import_module(f\".{name}\", __name__)\n raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n\n\n# #endregion ApiRoutesGetAttr\n# #endregion ApiRoutesModule\n" }, { "contract_id": "Route_Group_Contracts", "contract_type": "Block", "file_path": "backend/src/api/routes/__init__.py", - "start_line": 8, - "end_line": 39, + "start_line": 10, + "end_line": 43, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Declare the canonical route-module registry used by lazy imports and app router inclusion." + "DATA_CONTRACT": "Package -> RouterModule mapping", + "PURPOSE": "Declare the canonical route-module registry used by lazy imports and app router inclusion.", + "SIDE_EFFECT": "Registers route group imports via __getattr__" }, "relations": [ { @@ -582,17 +484,66 @@ "target_ref": "[LlmRoutes]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Block' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Block" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is not allowed for contract type 'Block'", + "detail": { + "actual_type": "Block", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Block' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Block" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region Route_Group_Contracts [C:3] [TYPE Block]\n# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion.\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ConnectionsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n__all__ = [\n \"admin\",\n \"assistant\",\n \"clean_release\",\n \"clean_release_v2\",\n \"connections\",\n \"dashboards\",\n \"dataset_review\",\n \"datasets\",\n \"environments\",\n \"git\",\n \"health\",\n \"llm\",\n \"mappings\",\n \"migration\",\n \"plugins\",\n \"profile\",\n \"reports\",\n \"settings\",\n \"storage\",\n \"tasks\",\n \"translate\",\n]\n# #endregion Route_Group_Contracts\n" + "body": "# #region Route_Group_Contracts [C:3] [TYPE Block]\n# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion.\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ConnectionsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n# @SIDE_EFFECT: Registers route group imports via __getattr__\n# @DATA_CONTRACT: Package -> RouterModule mapping\n__all__ = [\n \"admin\",\n \"assistant\",\n \"clean_release\",\n \"clean_release_v2\",\n \"connections\",\n \"dashboards\",\n \"dataset_review\",\n \"datasets\",\n \"environments\",\n \"git\",\n \"health\",\n \"llm\",\n \"mappings\",\n \"migration\",\n \"plugins\",\n \"profile\",\n \"reports\",\n \"settings\",\n \"storage\",\n \"tasks\",\n \"translate\",\n]\n# #endregion Route_Group_Contracts\n" }, { "contract_id": "ApiRoutesGetAttr", "contract_type": "Function", "file_path": "backend/src/api/routes/__init__.py", - "start_line": 42, - "end_line": 55, + "start_line": 46, + "end_line": 59, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -6786,10 +6737,10 @@ "file_path": "backend/src/api/routes/admin.py", "start_line": 1, "end_line": 366, - "tier": "TIER_2", - "complexity": 3, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "INVARIANT": "All endpoints in this module require 'Admin' role or 'admin' scope.", "LAYER": "API", "PURPOSE": "Admin API endpoints for user and role management." @@ -6816,18 +6767,45 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AdminApi [C:3] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user]\n#\n# @BRIEF Admin API endpoints for user and role management.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AuthRepository:Class]\n# @RELATION DEPENDS_ON -> [get_auth_db:Function]\n# @RELATION DEPENDS_ON -> [has_permission:Function]\n#\n# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope.\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.auth.repository import AuthRepository\nfrom ...core.auth.security import get_password_hash\nfrom ...core.database import get_auth_db\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...models.auth import ADGroupMapping, Role, User\nfrom ...schemas.auth import (\n ADGroupMappingCreate,\n ADGroupMappingSchema,\n PermissionSchema,\n RoleCreate,\n RoleSchema,\n RoleUpdate,\n UserCreate,\n UserUpdate,\n)\nfrom ...schemas.auth import (\n User as UserSchema,\n)\nfrom ...services.rbac_permission_catalog import (\n discover_declared_permissions,\n sync_permission_catalog,\n)\n\n# #region router [TYPE Variable]\n# @RELATION DEPENDS_ON -> fastapi.APIRouter\n# @BRIEF APIRouter instance for admin routes.\nrouter = APIRouter(prefix=\"/api/admin\", tags=[\"admin\"])\n# #endregion router\n\n\n# #region list_users [C:3] [TYPE Function]\n# @BRIEF Lists all registered users.\n# @PRE: Current user has 'Admin' role.\n# @POST: Returns a list of UserSchema objects.\n# @RELATION CALLS -> User\n@router.get(\"/users\", response_model=list[UserSchema])\nasync def list_users(\n db: Session = Depends(get_auth_db), _=Depends(has_permission(\"admin:users\", \"READ\"))\n):\n with belief_scope(\"api.admin.list_users\"):\n users = db.query(User).all()\n return users\n\n\n# #endregion list_users\n\n\n# #region create_user [C:3] [TYPE Function]\n# @BRIEF Creates a new local user.\n# @PRE: Current user has 'Admin' role.\n# @POST: New user is created in the database.\n# @RELATION CALLS -> [AuthRepository:Class]\n@router.post(\"/users\", response_model=UserSchema, status_code=status.HTTP_201_CREATED)\nasync def create_user(\n user_in: UserCreate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:users\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.create_user\"):\n repo = AuthRepository(db)\n if repo.get_user_by_username(user_in.username):\n raise HTTPException(status_code=400, detail=\"Username already exists\")\n\n new_user = User(\n username=user_in.username,\n email=user_in.email,\n password_hash=get_password_hash(user_in.password),\n auth_source=\"LOCAL\",\n is_active=user_in.is_active,\n )\n\n for role_name in user_in.roles:\n role = repo.get_role_by_name(role_name)\n if role:\n new_user.roles.append(role)\n\n db.add(new_user)\n db.commit()\n db.refresh(new_user)\n return new_user\n\n\n# #endregion create_user\n\n\n# #region update_user [C:3] [TYPE Function]\n# @BRIEF Updates an existing user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is updated in the database.\n# @RELATION CALLS -> AuthRepository\n@router.put(\"/users/{user_id}\", response_model=UserSchema)\nasync def update_user(\n user_id: str,\n user_in: UserUpdate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:users\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.update_user\"):\n repo = AuthRepository(db)\n user = repo.get_user_by_id(user_id)\n if not user:\n raise HTTPException(status_code=404, detail=\"User not found\")\n\n if user_in.email is not None:\n user.email = user_in.email\n if user_in.is_active is not None:\n user.is_active = user_in.is_active\n if user_in.password is not None:\n user.password_hash = get_password_hash(user_in.password)\n\n if user_in.roles is not None:\n user.roles = []\n for role_name in user_in.roles:\n role = repo.get_role_by_name(role_name)\n if role:\n user.roles.append(role)\n\n db.commit()\n db.refresh(user)\n return user\n\n\n# #endregion update_user\n\n\n# #region delete_user [C:3] [TYPE Function]\n# @BRIEF Deletes a user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is removed from the database.\n# @RELATION CALLS -> AuthRepository\n@router.delete(\"/users/{user_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_user(\n user_id: str,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:users\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.delete_user\"):\n logger.info(\n f\"[DEBUG] Attempting to delete user context={{'user_id': '{user_id}'}}\"\n )\n repo = AuthRepository(db)\n user = repo.get_user_by_id(user_id)\n if not user:\n logger.warning(\n f\"[DEBUG] User not found for deletion context={{'user_id': '{user_id}'}}\"\n )\n raise HTTPException(status_code=404, detail=\"User not found\")\n\n logger.info(\n f\"[DEBUG] Found user to delete context={{'username': '{user.username}'}}\"\n )\n db.delete(user)\n db.commit()\n logger.info(\n f\"[DEBUG] Successfully deleted user context={{'user_id': '{user_id}'}}\"\n )\n return None\n\n\n# #endregion delete_user\n\n\n# #region list_roles [C:3] [TYPE Function]\n# @BRIEF Lists all available roles.\n# @RELATION CALLS -> [Role:Class]\n@router.get(\"/roles\", response_model=list[RoleSchema])\nasync def list_roles(\n db: Session = Depends(get_auth_db), _=Depends(has_permission(\"admin:roles\", \"READ\"))\n):\n with belief_scope(\"api.admin.list_roles\"):\n return db.query(Role).all()\n\n\n# #endregion list_roles\n\n\n# #region create_role [C:3] [TYPE Function]\n# @BRIEF Creates a new system role with associated permissions.\n# @PRE: Role name must be unique.\n# @POST: New Role record is created in auth.db.\n# @SIDE_EFFECT: Commits new role and associations to auth.db.\n# @RELATION CALLS -> [get_permission_by_id:Function]\n@router.post(\"/roles\", response_model=RoleSchema, status_code=status.HTTP_201_CREATED)\nasync def create_role(\n role_in: RoleCreate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:roles\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.create_role\"):\n if db.query(Role).filter(Role.name == role_in.name).first():\n raise HTTPException(status_code=400, detail=\"Role already exists\")\n\n new_role = Role(name=role_in.name, description=role_in.description)\n repo = AuthRepository(db)\n\n for perm_id_or_str in role_in.permissions:\n perm = repo.get_permission_by_id(perm_id_or_str)\n if not perm and \":\" in perm_id_or_str:\n res, act = perm_id_or_str.split(\":\", 1)\n perm = repo.get_permission_by_resource_action(res, act)\n\n if perm:\n new_role.permissions.append(perm)\n\n db.add(new_role)\n db.commit()\n db.refresh(new_role)\n return new_role\n\n\n# #endregion create_role\n\n\n# #region update_role [C:3] [TYPE Function]\n# @BRIEF Updates an existing role's metadata and permissions.\n# @PRE: role_id must be a valid existing role UUID.\n# @POST: Role record is updated in auth.db.\n# @SIDE_EFFECT: Commits updates to auth.db.\n# @RELATION CALLS -> [get_role_by_id:Function]\n@router.put(\"/roles/{role_id}\", response_model=RoleSchema)\nasync def update_role(\n role_id: str,\n role_in: RoleUpdate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:roles\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.update_role\"):\n repo = AuthRepository(db)\n role = repo.get_role_by_id(role_id)\n if not role:\n raise HTTPException(status_code=404, detail=\"Role not found\")\n\n if role_in.name is not None:\n role.name = role_in.name\n if role_in.description is not None:\n role.description = role_in.description\n\n if role_in.permissions is not None:\n role.permissions = []\n for perm_id_or_str in role_in.permissions:\n perm = repo.get_permission_by_id(perm_id_or_str)\n if not perm and \":\" in perm_id_or_str:\n res, act = perm_id_or_str.split(\":\", 1)\n perm = repo.get_permission_by_resource_action(res, act)\n\n if perm:\n role.permissions.append(perm)\n\n db.commit()\n db.refresh(role)\n return role\n\n\n# #endregion update_role\n\n\n# #region delete_role [C:3] [TYPE Function]\n# @BRIEF Removes a role from the system.\n# @PRE: role_id must be a valid existing role UUID.\n# @POST: Role record is removed from auth.db.\n# @SIDE_EFFECT: Deletes record from auth.db and commits.\n# @RELATION CALLS -> [get_role_by_id:Function]\n@router.delete(\"/roles/{role_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_role(\n role_id: str,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:roles\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.delete_role\"):\n repo = AuthRepository(db)\n role = repo.get_role_by_id(role_id)\n if not role:\n raise HTTPException(status_code=404, detail=\"Role not found\")\n\n db.delete(role)\n db.commit()\n return None\n\n\n# #endregion delete_role\n\n\n# #region list_permissions [C:3] [TYPE Function]\n# @BRIEF Lists all available system permissions for assignment.\n# @POST: Returns a list of all PermissionSchema objects.\n# @RELATION CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions\n@router.get(\"/permissions\", response_model=list[PermissionSchema])\nasync def list_permissions(\n db: Session = Depends(get_auth_db),\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"admin:roles\", \"READ\")),\n):\n with belief_scope(\"api.admin.list_permissions\"):\n declared_permissions = discover_declared_permissions(\n plugin_loader=plugin_loader\n )\n inserted_count = sync_permission_catalog(\n db=db, declared_permissions=declared_permissions\n )\n if inserted_count > 0:\n logger.reason(\n f\"Synchronized {inserted_count} missing RBAC permissions into auth catalog\",\n extra={\"src\": \"api.admin.list_permissions\"},\n )\n\n repo = AuthRepository(db)\n return repo.list_permissions()\n\n\n# #endregion list_permissions\n\n\n# #region list_ad_mappings [C:3] [TYPE Function]\n# @BRIEF Lists all AD Group to Role mappings.\n# @RELATION CALLS -> ADGroupMapping\n@router.get(\"/ad-mappings\", response_model=list[ADGroupMappingSchema])\nasync def list_ad_mappings(\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"api.admin.list_ad_mappings\"):\n return db.query(ADGroupMapping).all()\n\n\n# #endregion list_ad_mappings\n\n\n# #region create_ad_mapping [C:2] [TYPE Function]\n# @RELATION DEPENDS_ON -> [ADGroupMapping:Class]\n# @RELATION DEPENDS_ON -> [get_auth_db:Function]\n# @RELATION DEPENDS_ON -> [has_permission:Function]\n# @BRIEF Creates a new AD Group mapping.\n@router.post(\"/ad-mappings\", response_model=ADGroupMappingSchema)\nasync def create_ad_mapping(\n mapping_in: ADGroupMappingCreate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.create_ad_mapping\"):\n new_mapping = ADGroupMapping(\n ad_group=mapping_in.ad_group, role_id=mapping_in.role_id\n )\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n\n\n# #endregion create_ad_mapping\n\n# #endregion AdminApi\n" + "body": "# #region AdminApi [C:5] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user]\n#\n# @BRIEF Admin API endpoints for user and role management.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AuthRepository:Class]\n# @RELATION DEPENDS_ON -> [get_auth_db:Function]\n# @RELATION DEPENDS_ON -> [has_permission:Function]\n#\n# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope.\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.auth.repository import AuthRepository\nfrom ...core.auth.security import get_password_hash\nfrom ...core.database import get_auth_db\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...models.auth import ADGroupMapping, Role, User\nfrom ...schemas.auth import (\n ADGroupMappingCreate,\n ADGroupMappingSchema,\n PermissionSchema,\n RoleCreate,\n RoleSchema,\n RoleUpdate,\n UserCreate,\n UserUpdate,\n)\nfrom ...schemas.auth import (\n User as UserSchema,\n)\nfrom ...services.rbac_permission_catalog import (\n discover_declared_permissions,\n sync_permission_catalog,\n)\n\n# #region router [TYPE Variable]\n# @RELATION DEPENDS_ON -> fastapi.APIRouter\n# @BRIEF APIRouter instance for admin routes.\nrouter = APIRouter(prefix=\"/api/admin\", tags=[\"admin\"])\n# #endregion router\n\n\n# #region list_users [C:3] [TYPE Function]\n# @BRIEF Lists all registered users.\n# @PRE: Current user has 'Admin' role.\n# @POST: Returns a list of UserSchema objects.\n# @RELATION CALLS -> User\n@router.get(\"/users\", response_model=list[UserSchema])\nasync def list_users(\n db: Session = Depends(get_auth_db), _=Depends(has_permission(\"admin:users\", \"READ\"))\n):\n with belief_scope(\"api.admin.list_users\"):\n users = db.query(User).all()\n return users\n\n\n# #endregion list_users\n\n\n# #region create_user [C:3] [TYPE Function]\n# @BRIEF Creates a new local user.\n# @PRE: Current user has 'Admin' role.\n# @POST: New user is created in the database.\n# @RELATION CALLS -> [AuthRepository:Class]\n@router.post(\"/users\", response_model=UserSchema, status_code=status.HTTP_201_CREATED)\nasync def create_user(\n user_in: UserCreate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:users\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.create_user\"):\n repo = AuthRepository(db)\n if repo.get_user_by_username(user_in.username):\n raise HTTPException(status_code=400, detail=\"Username already exists\")\n\n new_user = User(\n username=user_in.username,\n email=user_in.email,\n password_hash=get_password_hash(user_in.password),\n auth_source=\"LOCAL\",\n is_active=user_in.is_active,\n )\n\n for role_name in user_in.roles:\n role = repo.get_role_by_name(role_name)\n if role:\n new_user.roles.append(role)\n\n db.add(new_user)\n db.commit()\n db.refresh(new_user)\n return new_user\n\n\n# #endregion create_user\n\n\n# #region update_user [C:3] [TYPE Function]\n# @BRIEF Updates an existing user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is updated in the database.\n# @RELATION CALLS -> AuthRepository\n@router.put(\"/users/{user_id}\", response_model=UserSchema)\nasync def update_user(\n user_id: str,\n user_in: UserUpdate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:users\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.update_user\"):\n repo = AuthRepository(db)\n user = repo.get_user_by_id(user_id)\n if not user:\n raise HTTPException(status_code=404, detail=\"User not found\")\n\n if user_in.email is not None:\n user.email = user_in.email\n if user_in.is_active is not None:\n user.is_active = user_in.is_active\n if user_in.password is not None:\n user.password_hash = get_password_hash(user_in.password)\n\n if user_in.roles is not None:\n user.roles = []\n for role_name in user_in.roles:\n role = repo.get_role_by_name(role_name)\n if role:\n user.roles.append(role)\n\n db.commit()\n db.refresh(user)\n return user\n\n\n# #endregion update_user\n\n\n# #region delete_user [C:3] [TYPE Function]\n# @BRIEF Deletes a user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is removed from the database.\n# @RELATION CALLS -> AuthRepository\n@router.delete(\"/users/{user_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_user(\n user_id: str,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:users\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.delete_user\"):\n logger.info(\n f\"[DEBUG] Attempting to delete user context={{'user_id': '{user_id}'}}\"\n )\n repo = AuthRepository(db)\n user = repo.get_user_by_id(user_id)\n if not user:\n logger.warning(\n f\"[DEBUG] User not found for deletion context={{'user_id': '{user_id}'}}\"\n )\n raise HTTPException(status_code=404, detail=\"User not found\")\n\n logger.info(\n f\"[DEBUG] Found user to delete context={{'username': '{user.username}'}}\"\n )\n db.delete(user)\n db.commit()\n logger.info(\n f\"[DEBUG] Successfully deleted user context={{'user_id': '{user_id}'}}\"\n )\n return None\n\n\n# #endregion delete_user\n\n\n# #region list_roles [C:3] [TYPE Function]\n# @BRIEF Lists all available roles.\n# @RELATION CALLS -> [Role:Class]\n@router.get(\"/roles\", response_model=list[RoleSchema])\nasync def list_roles(\n db: Session = Depends(get_auth_db), _=Depends(has_permission(\"admin:roles\", \"READ\"))\n):\n with belief_scope(\"api.admin.list_roles\"):\n return db.query(Role).all()\n\n\n# #endregion list_roles\n\n\n# #region create_role [C:3] [TYPE Function]\n# @BRIEF Creates a new system role with associated permissions.\n# @PRE: Role name must be unique.\n# @POST: New Role record is created in auth.db.\n# @SIDE_EFFECT: Commits new role and associations to auth.db.\n# @RELATION CALLS -> [get_permission_by_id:Function]\n@router.post(\"/roles\", response_model=RoleSchema, status_code=status.HTTP_201_CREATED)\nasync def create_role(\n role_in: RoleCreate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:roles\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.create_role\"):\n if db.query(Role).filter(Role.name == role_in.name).first():\n raise HTTPException(status_code=400, detail=\"Role already exists\")\n\n new_role = Role(name=role_in.name, description=role_in.description)\n repo = AuthRepository(db)\n\n for perm_id_or_str in role_in.permissions:\n perm = repo.get_permission_by_id(perm_id_or_str)\n if not perm and \":\" in perm_id_or_str:\n res, act = perm_id_or_str.split(\":\", 1)\n perm = repo.get_permission_by_resource_action(res, act)\n\n if perm:\n new_role.permissions.append(perm)\n\n db.add(new_role)\n db.commit()\n db.refresh(new_role)\n return new_role\n\n\n# #endregion create_role\n\n\n# #region update_role [C:3] [TYPE Function]\n# @BRIEF Updates an existing role's metadata and permissions.\n# @PRE: role_id must be a valid existing role UUID.\n# @POST: Role record is updated in auth.db.\n# @SIDE_EFFECT: Commits updates to auth.db.\n# @RELATION CALLS -> [get_role_by_id:Function]\n@router.put(\"/roles/{role_id}\", response_model=RoleSchema)\nasync def update_role(\n role_id: str,\n role_in: RoleUpdate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:roles\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.update_role\"):\n repo = AuthRepository(db)\n role = repo.get_role_by_id(role_id)\n if not role:\n raise HTTPException(status_code=404, detail=\"Role not found\")\n\n if role_in.name is not None:\n role.name = role_in.name\n if role_in.description is not None:\n role.description = role_in.description\n\n if role_in.permissions is not None:\n role.permissions = []\n for perm_id_or_str in role_in.permissions:\n perm = repo.get_permission_by_id(perm_id_or_str)\n if not perm and \":\" in perm_id_or_str:\n res, act = perm_id_or_str.split(\":\", 1)\n perm = repo.get_permission_by_resource_action(res, act)\n\n if perm:\n role.permissions.append(perm)\n\n db.commit()\n db.refresh(role)\n return role\n\n\n# #endregion update_role\n\n\n# #region delete_role [C:3] [TYPE Function]\n# @BRIEF Removes a role from the system.\n# @PRE: role_id must be a valid existing role UUID.\n# @POST: Role record is removed from auth.db.\n# @SIDE_EFFECT: Deletes record from auth.db and commits.\n# @RELATION CALLS -> [get_role_by_id:Function]\n@router.delete(\"/roles/{role_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_role(\n role_id: str,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:roles\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.delete_role\"):\n repo = AuthRepository(db)\n role = repo.get_role_by_id(role_id)\n if not role:\n raise HTTPException(status_code=404, detail=\"Role not found\")\n\n db.delete(role)\n db.commit()\n return None\n\n\n# #endregion delete_role\n\n\n# #region list_permissions [C:3] [TYPE Function]\n# @BRIEF Lists all available system permissions for assignment.\n# @POST: Returns a list of all PermissionSchema objects.\n# @RELATION CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions\n@router.get(\"/permissions\", response_model=list[PermissionSchema])\nasync def list_permissions(\n db: Session = Depends(get_auth_db),\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"admin:roles\", \"READ\")),\n):\n with belief_scope(\"api.admin.list_permissions\"):\n declared_permissions = discover_declared_permissions(\n plugin_loader=plugin_loader\n )\n inserted_count = sync_permission_catalog(\n db=db, declared_permissions=declared_permissions\n )\n if inserted_count > 0:\n logger.reason(\n f\"Synchronized {inserted_count} missing RBAC permissions into auth catalog\",\n extra={\"src\": \"api.admin.list_permissions\"},\n )\n\n repo = AuthRepository(db)\n return repo.list_permissions()\n\n\n# #endregion list_permissions\n\n\n# #region list_ad_mappings [C:3] [TYPE Function]\n# @BRIEF Lists all AD Group to Role mappings.\n# @RELATION CALLS -> ADGroupMapping\n@router.get(\"/ad-mappings\", response_model=list[ADGroupMappingSchema])\nasync def list_ad_mappings(\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"api.admin.list_ad_mappings\"):\n return db.query(ADGroupMapping).all()\n\n\n# #endregion list_ad_mappings\n\n\n# #region create_ad_mapping [C:2] [TYPE Function]\n# @RELATION DEPENDS_ON -> [ADGroupMapping:Class]\n# @RELATION DEPENDS_ON -> [get_auth_db:Function]\n# @RELATION DEPENDS_ON -> [has_permission:Function]\n# @BRIEF Creates a new AD Group mapping.\n@router.post(\"/ad-mappings\", response_model=ADGroupMappingSchema)\nasync def create_ad_mapping(\n mapping_in: ADGroupMappingCreate,\n db: Session = Depends(get_auth_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"api.admin.create_ad_mapping\"):\n new_mapping = ADGroupMapping(\n ad_group=mapping_in.ad_group, role_id=mapping_in.role_id\n )\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n\n\n# #endregion create_ad_mapping\n\n# #endregion AdminApi\n" }, { "contract_id": "list_users", @@ -8853,14 +8831,18 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", "start_line": 1, - "end_line": 292, - "tier": "TIER_2", - "complexity": 3, + "end_line": 296, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "UserPermissions -> ToolCatalog", "INVARIANT": "Tool catalog is filtered by user permissions before being sent to LLM.", "LAYER": "API", - "PURPOSE": "LLM-based intent planning, tool catalog construction, and authorization for the assistant API." + "POST": "LLM tool catalog filtered and returned", + "PRE": "Assistant routes initialized, user authenticated", + "PURPOSE": "LLM-based intent planning, tool catalog construction, and authorization for the assistant API.", + "SIDE_EFFECT": "Filters tool catalog by user permissions" }, "relations": [ { @@ -8882,27 +8864,17 @@ "target_ref": "[AssistantLlmPlannerIntent]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantLlmPlanner [C:3] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog]\n# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent]\n# @INVARIANT: Tool catalog is filtered by user permissions before being sent to LLM.\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.dependencies import has_permission\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import resolve_bound_provider_id\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._resolvers import (\n _get_default_environment_id,\n)\nfrom ._schemas import (\n INTENT_PERMISSION_CHECKS,\n)\n\n\n# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE: checks list contains resource-action tuples.\n# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n\n\n# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Check whether user has at least one permission tuple from the provided list.\n# @PRE: current_user and checks list are valid.\n# @POST: Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n\n\n# #region _build_tool_catalog [C:3] [TYPE Function]\n# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.\n# @PRE: current_user is authenticated; config/db are available.\n# @POST: Returns list of executable tools filtered by permission and runtime availability.\n# @RELATION CALLS -> LLMProviderService\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n envs = config_manager.get_environments()\n default_env_id = _get_default_environment_id(config_manager)\n providers = LLMProviderService(db).get_all_providers()\n llm_settings = {}\n try:\n llm_settings = config_manager.get_config().settings.llm\n except Exception:\n llm_settings = {}\n active_provider = next((p.id for p in providers if p.is_active), None)\n fallback_provider = active_provider or (providers[0].id if providers else None)\n validation_provider = (\n resolve_bound_provider_id(llm_settings, \"dashboard_validation\")\n or fallback_provider\n )\n documentation_provider = (\n resolve_bound_provider_id(llm_settings, \"documentation\") or fallback_provider\n )\n\n candidates: list[dict[str, Any]] = [\n {\n \"operation\": \"show_capabilities\",\n \"domain\": \"assistant\",\n \"description\": \"Show available assistant commands and examples\",\n \"required_entities\": [],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_task_status\",\n \"domain\": \"status\",\n \"description\": \"Get task status by task_id or latest user task\",\n \"required_entities\": [],\n \"optional_entities\": [\"task_id\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"create_branch\",\n \"domain\": \"git\",\n \"description\": \"Create git branch for dashboard by id/slug/title\",\n \"required_entities\": [\"branch_name\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"commit_changes\",\n \"domain\": \"git\",\n \"description\": \"Commit dashboard repository changes by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\", \"message\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"deploy_dashboard\",\n \"domain\": \"git\",\n \"description\": \"Deploy dashboard (id/slug/title) to target environment\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"execute_migration\",\n \"domain\": \"migration\",\n \"description\": \"Run dashboard migration (id/slug/title) between environments. Optional boolean flags: replace_db_config, fix_cross_filters\",\n \"required_entities\": [\"source_env\", \"target_env\"],\n \"optional_entities\": [\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_backup\",\n \"domain\": \"backup\",\n \"description\": \"Run backup for environment or specific dashboard by id/slug/title\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_validation\",\n \"domain\": \"llm\",\n \"description\": \"Run LLM dashboard validation by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_ref\", \"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": validation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_documentation\",\n \"domain\": \"llm\",\n \"description\": \"Generate dataset documentation via LLM\",\n \"required_entities\": [\"dataset_id\"],\n \"optional_entities\": [\"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": documentation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_health_summary\",\n \"domain\": \"health\",\n \"description\": \"Get summary of dashboard health and failing validations\",\n \"required_entities\": [],\n \"optional_entities\": [\"environment\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n ]\n\n available: list[dict[str, Any]] = []\n for tool in candidates:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n\n if dataset_review_context is not None:\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n\n\n# #region _coerce_intent_entities [C:2] [TYPE Function]\n# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.\n# @PRE: intent contains entities dict or missing entities.\n# @POST: Returned intent has numeric ids coerced where possible and string values stripped.\ndef _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:\n entities = intent.get(\"entities\")\n if not isinstance(entities, dict):\n intent[\"entities\"] = {}\n entities = intent[\"entities\"]\n for key in (\"dashboard_id\", \"dataset_id\"):\n value = entities.get(key)\n if isinstance(value, str) and value.strip().isdigit():\n entities[key] = int(value.strip())\n for key, value in list(entities.items()):\n if isinstance(value, str):\n entities[key] = value.strip()\n return intent\n\n\n# #endregion _coerce_intent_entities\n\n\n# #endregion AssistantLlmPlanner\n" + "body": "# #region AssistantLlmPlanner [C:5] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog]\n# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent]\n# @PRE: Assistant routes initialized, user authenticated\n# @POST: LLM tool catalog filtered and returned\n# @INVARIANT: Tool catalog is filtered by user permissions before being sent to LLM.\n# @SIDE_EFFECT: Filters tool catalog by user permissions\n# @DATA_CONTRACT: UserPermissions -> ToolCatalog\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.dependencies import has_permission\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import resolve_bound_provider_id\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._resolvers import (\n _get_default_environment_id,\n)\nfrom ._schemas import (\n INTENT_PERMISSION_CHECKS,\n)\n\n\n# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE: checks list contains resource-action tuples.\n# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n\n\n# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Check whether user has at least one permission tuple from the provided list.\n# @PRE: current_user and checks list are valid.\n# @POST: Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n\n\n# #region _build_tool_catalog [C:3] [TYPE Function]\n# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.\n# @PRE: current_user is authenticated; config/db are available.\n# @POST: Returns list of executable tools filtered by permission and runtime availability.\n# @RELATION CALLS -> LLMProviderService\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n envs = config_manager.get_environments()\n default_env_id = _get_default_environment_id(config_manager)\n providers = LLMProviderService(db).get_all_providers()\n llm_settings = {}\n try:\n llm_settings = config_manager.get_config().settings.llm\n except Exception:\n llm_settings = {}\n active_provider = next((p.id for p in providers if p.is_active), None)\n fallback_provider = active_provider or (providers[0].id if providers else None)\n validation_provider = (\n resolve_bound_provider_id(llm_settings, \"dashboard_validation\")\n or fallback_provider\n )\n documentation_provider = (\n resolve_bound_provider_id(llm_settings, \"documentation\") or fallback_provider\n )\n\n candidates: list[dict[str, Any]] = [\n {\n \"operation\": \"show_capabilities\",\n \"domain\": \"assistant\",\n \"description\": \"Show available assistant commands and examples\",\n \"required_entities\": [],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_task_status\",\n \"domain\": \"status\",\n \"description\": \"Get task status by task_id or latest user task\",\n \"required_entities\": [],\n \"optional_entities\": [\"task_id\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"create_branch\",\n \"domain\": \"git\",\n \"description\": \"Create git branch for dashboard by id/slug/title\",\n \"required_entities\": [\"branch_name\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"commit_changes\",\n \"domain\": \"git\",\n \"description\": \"Commit dashboard repository changes by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\", \"message\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"deploy_dashboard\",\n \"domain\": \"git\",\n \"description\": \"Deploy dashboard (id/slug/title) to target environment\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"execute_migration\",\n \"domain\": \"migration\",\n \"description\": \"Run dashboard migration (id/slug/title) between environments. Optional boolean flags: replace_db_config, fix_cross_filters\",\n \"required_entities\": [\"source_env\", \"target_env\"],\n \"optional_entities\": [\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_backup\",\n \"domain\": \"backup\",\n \"description\": \"Run backup for environment or specific dashboard by id/slug/title\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_validation\",\n \"domain\": \"llm\",\n \"description\": \"Run LLM dashboard validation by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_ref\", \"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": validation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_documentation\",\n \"domain\": \"llm\",\n \"description\": \"Generate dataset documentation via LLM\",\n \"required_entities\": [\"dataset_id\"],\n \"optional_entities\": [\"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": documentation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_health_summary\",\n \"domain\": \"health\",\n \"description\": \"Get summary of dashboard health and failing validations\",\n \"required_entities\": [],\n \"optional_entities\": [\"environment\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n ]\n\n available: list[dict[str, Any]] = []\n for tool in candidates:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n\n if dataset_review_context is not None:\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n\n\n# #region _coerce_intent_entities [C:2] [TYPE Function]\n# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.\n# @PRE: intent contains entities dict or missing entities.\n# @POST: Returned intent has numeric ids coerced where possible and string values stripped.\ndef _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:\n entities = intent.get(\"entities\")\n if not isinstance(entities, dict):\n intent[\"entities\"] = {}\n entities = intent[\"entities\"]\n for key in (\"dashboard_id\", \"dataset_id\"):\n value = entities.get(key)\n if isinstance(value, str) and value.strip().isdigit():\n entities[key] = int(value.strip())\n for key, value in list(entities.items()):\n if isinstance(value, str):\n entities[key] = value.strip()\n return intent\n\n\n# #endregion _coerce_intent_entities\n\n\n# #endregion AssistantLlmPlanner\n" }, { "contract_id": "_check_any_permission", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 30, - "end_line": 50, + "start_line": 34, + "end_line": 54, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -8940,8 +8912,8 @@ "contract_id": "_has_any_permission", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 53, - "end_line": 65, + "start_line": 57, + "end_line": 69, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -8979,8 +8951,8 @@ "contract_id": "_build_tool_catalog", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 68, - "end_line": 267, + "start_line": 72, + "end_line": 271, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9025,8 +8997,8 @@ "contract_id": "_coerce_intent_entities", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 270, - "end_line": 289, + "start_line": 274, + "end_line": 293, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -9065,14 +9037,18 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", "start_line": 1, - "end_line": 170, - "tier": "TIER_2", - "complexity": 3, + "end_line": 174, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "UserIntent -> PlannedAction", "INVARIANT": "Production deployments always require confirmation.", "LAYER": "API", - "PURPOSE": "LLM-based intent planning and authorization for the assistant API — separated from tool catalog." + "POST": "Intent planning registered with confirmation gate", + "PRE": "Assistant routes initialized, user authenticated", + "PURPOSE": "LLM-based intent planning and authorization for the assistant API — separated from tool catalog.", + "SIDE_EFFECT": "Registers intent planning routes" }, "relations": [ { @@ -9088,27 +9064,17 @@ "target_ref": "[AssistantResolvers]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantLlmPlannerIntent [C:3] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization]\n# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @INVARIANT: Production deployments always require confirmation.\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom src.plugins.llm_analysis.service import LLMClient\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import (\n normalize_llm_settings,\n)\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._llm_planner import (\n _check_any_permission,\n _coerce_intent_entities,\n)\nfrom ._resolvers import (\n _is_production_env,\n _resolve_provider_id,\n)\nfrom ._schemas import INTENT_PERMISSION_CHECKS\n\n\n# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE: tools list contains allowed operations for current user.\n# @POST: Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a deterministic intent planner for backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n\n\n# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE: intent.operation is present for known assistant command domains.\n# @POST: Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n if operation in INTENT_PERMISSION_CHECKS:\n _check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation])\n\n\n# #endregion _authorize_intent\n\n\n# #endregion AssistantLlmPlannerIntent\n" + "body": "# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization]\n# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @PRE: Assistant routes initialized, user authenticated\n# @POST: Intent planning registered with confirmation gate\n# @INVARIANT: Production deployments always require confirmation.\n# @SIDE_EFFECT: Registers intent planning routes\n# @DATA_CONTRACT: UserIntent -> PlannedAction\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom src.plugins.llm_analysis.service import LLMClient\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import (\n normalize_llm_settings,\n)\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._llm_planner import (\n _check_any_permission,\n _coerce_intent_entities,\n)\nfrom ._resolvers import (\n _is_production_env,\n _resolve_provider_id,\n)\nfrom ._schemas import INTENT_PERMISSION_CHECKS\n\n\n# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE: tools list contains allowed operations for current user.\n# @POST: Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a deterministic intent planner for backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n\n\n# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE: intent.operation is present for known assistant command domains.\n# @POST: Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n if operation in INTENT_PERMISSION_CHECKS:\n _check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation])\n\n\n# #endregion _authorize_intent\n\n\n# #endregion AssistantLlmPlannerIntent\n" }, { "contract_id": "_plan_intent_with_llm", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", - "start_line": 36, - "end_line": 154, + "start_line": 40, + "end_line": 158, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -9146,8 +9112,8 @@ "contract_id": "_authorize_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", - "start_line": 157, - "end_line": 167, + "start_line": 161, + "end_line": 171, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -10919,7 +10885,7 @@ "complexity": 4, "metadata": { "COMPLEXITY": 4, - "LAYER": "UI (API)", + "LAYER": "API", "POST": "Candidate registration, approval, publication, and revocation routes are registered without behavior changes.", "PRE": "Clean release repository dependency is available for candidate lifecycle endpoints.", "PURPOSE": "Redesigned clean release API for headless candidate lifecycle.", @@ -10945,28 +10911,10 @@ "target_ref": "[publish_candidate]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CleanReleaseV2Api [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, candidate, lifecycle, api]\n# @BRIEF Redesigned clean release API for headless candidate lifecycle.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION CALLS -> [approve_candidate]\n# @RELATION CALLS -> [publish_candidate]\n# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.\n# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.\n# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\n\nfrom ...dependencies import get_clean_release_repository\nfrom ...models.clean_release import (\n CandidateArtifact,\n DistributionManifest,\n ReleaseCandidate,\n)\nfrom ...services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ...services.clean_release.dto import CandidateDTO, ManifestDTO\nfrom ...services.clean_release.enums import CandidateStatus\nfrom ...services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\nfrom ...services.clean_release.repository import CleanReleaseRepository\n\nrouter = APIRouter(prefix=\"/api/v2/clean-release\", tags=[\"Clean Release V2\"])\n\n\n# #region ApprovalRequest [C:1] [TYPE Class]\n# @BRIEF Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# #endregion ApprovalRequest\n\n\n# #region PublishRequest [C:1] [TYPE Class]\n# @BRIEF Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# #endregion PublishRequest\n\n\n# #region RevokeRequest [C:1] [TYPE Class]\n# @BRIEF Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# #endregion RevokeRequest\n\n\n# #region register_candidate [C:3] [TYPE Function]\n# @BRIEF Register a new release candidate.\n# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).\n# @POST: Candidate is saved in repository.\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION DEPENDS_ON -> [clean_release_dto]\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate(\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = ReleaseCandidate(\n id=payload[\"id\"],\n version=payload[\"version\"],\n source_snapshot_ref=payload[\"source_snapshot_ref\"],\n created_by=payload[\"created_by\"],\n created_at=datetime.now(UTC),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# #endregion register_candidate\n\n\n# #region import_artifacts [C:3] [TYPE Function]\n# @BRIEF Associate artifacts with a release candidate.\n# @PRE: Candidate exists.\n# @POST: Artifacts are processed (placeholder).\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_artifacts(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n for art_data in payload.get(\"artifacts\", []):\n artifact = CandidateArtifact(\n id=art_data[\"id\"],\n candidate_id=candidate_id,\n path=art_data[\"path\"],\n sha256=art_data[\"sha256\"],\n size=art_data[\"size\"],\n )\n # In a real repo we'd have save_artifact\n # repository.save_artifact(artifact)\n pass\n\n return {\"status\": \"success\"}\n\n\n# #endregion import_artifacts\n\n\n# #region build_manifest [C:3] [TYPE Function]\n# @BRIEF Generate distribution manifest for a candidate.\n# @PRE: Candidate exists.\n# @POST: Manifest is created and saved.\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_manifest(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n manifest = DistributionManifest(\n id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash-123\",\n artifacts_digest=\"art-hash-123\",\n created_by=\"system\",\n created_at=datetime.now(UTC),\n source_snapshot_ref=candidate.source_snapshot_ref,\n content_json={\"items\": [], \"summary\": {}},\n )\n repository.save_manifest(manifest)\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# #endregion build_manifest\n\n\n# #region approve_candidate_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to record candidate approval.\n# @RELATION CALLS -> [approve_candidate]\n@router.post(\"/candidates/{candidate_id}/approve\")\nasync def approve_candidate_endpoint(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# #endregion approve_candidate_endpoint\n\n\n# #region reject_candidate_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to record candidate rejection.\n# @RELATION CALLS -> [reject_candidate]\n@router.post(\"/candidates/{candidate_id}/reject\")\nasync def reject_candidate_endpoint(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# #endregion reject_candidate_endpoint\n\n\n# #region publish_candidate_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to publish an approved candidate.\n# @RELATION CALLS -> [publish_candidate]\n@router.post(\"/candidates/{candidate_id}/publish\")\nasync def publish_candidate_endpoint(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n published_by=str(payload[\"published_by\"]),\n target_channel=str(payload[\"target_channel\"]),\n publication_ref=payload.get(\"publication_ref\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# #endregion publish_candidate_endpoint\n\n\n# #region revoke_publication_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to revoke a previous publication.\n# @RELATION CALLS -> [revoke_publication]\n@router.post(\"/publications/{publication_id}/revoke\")\nasync def revoke_publication_endpoint(\n publication_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=publication_id,\n revoked_by=str(payload[\"revoked_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# #endregion revoke_publication_endpoint\n\n# #endregion CleanReleaseV2Api\n" + "body": "# #region CleanReleaseV2Api [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, candidate, lifecycle, api]\n# @BRIEF Redesigned clean release API for headless candidate lifecycle.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION CALLS -> [approve_candidate]\n# @RELATION CALLS -> [publish_candidate]\n# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.\n# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.\n# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\n\nfrom ...dependencies import get_clean_release_repository\nfrom ...models.clean_release import (\n CandidateArtifact,\n DistributionManifest,\n ReleaseCandidate,\n)\nfrom ...services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ...services.clean_release.dto import CandidateDTO, ManifestDTO\nfrom ...services.clean_release.enums import CandidateStatus\nfrom ...services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\nfrom ...services.clean_release.repository import CleanReleaseRepository\n\nrouter = APIRouter(prefix=\"/api/v2/clean-release\", tags=[\"Clean Release V2\"])\n\n\n# #region ApprovalRequest [C:1] [TYPE Class]\n# @BRIEF Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# #endregion ApprovalRequest\n\n\n# #region PublishRequest [C:1] [TYPE Class]\n# @BRIEF Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# #endregion PublishRequest\n\n\n# #region RevokeRequest [C:1] [TYPE Class]\n# @BRIEF Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# #endregion RevokeRequest\n\n\n# #region register_candidate [C:3] [TYPE Function]\n# @BRIEF Register a new release candidate.\n# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).\n# @POST: Candidate is saved in repository.\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION DEPENDS_ON -> [clean_release_dto]\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate(\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = ReleaseCandidate(\n id=payload[\"id\"],\n version=payload[\"version\"],\n source_snapshot_ref=payload[\"source_snapshot_ref\"],\n created_by=payload[\"created_by\"],\n created_at=datetime.now(UTC),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# #endregion register_candidate\n\n\n# #region import_artifacts [C:3] [TYPE Function]\n# @BRIEF Associate artifacts with a release candidate.\n# @PRE: Candidate exists.\n# @POST: Artifacts are processed (placeholder).\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_artifacts(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n for art_data in payload.get(\"artifacts\", []):\n artifact = CandidateArtifact(\n id=art_data[\"id\"],\n candidate_id=candidate_id,\n path=art_data[\"path\"],\n sha256=art_data[\"sha256\"],\n size=art_data[\"size\"],\n )\n # In a real repo we'd have save_artifact\n # repository.save_artifact(artifact)\n pass\n\n return {\"status\": \"success\"}\n\n\n# #endregion import_artifacts\n\n\n# #region build_manifest [C:3] [TYPE Function]\n# @BRIEF Generate distribution manifest for a candidate.\n# @PRE: Candidate exists.\n# @POST: Manifest is created and saved.\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_manifest(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n manifest = DistributionManifest(\n id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash-123\",\n artifacts_digest=\"art-hash-123\",\n created_by=\"system\",\n created_at=datetime.now(UTC),\n source_snapshot_ref=candidate.source_snapshot_ref,\n content_json={\"items\": [], \"summary\": {}},\n )\n repository.save_manifest(manifest)\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# #endregion build_manifest\n\n\n# #region approve_candidate_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to record candidate approval.\n# @RELATION CALLS -> [approve_candidate]\n@router.post(\"/candidates/{candidate_id}/approve\")\nasync def approve_candidate_endpoint(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# #endregion approve_candidate_endpoint\n\n\n# #region reject_candidate_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to record candidate rejection.\n# @RELATION CALLS -> [reject_candidate]\n@router.post(\"/candidates/{candidate_id}/reject\")\nasync def reject_candidate_endpoint(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# #endregion reject_candidate_endpoint\n\n\n# #region publish_candidate_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to publish an approved candidate.\n# @RELATION CALLS -> [publish_candidate]\n@router.post(\"/candidates/{candidate_id}/publish\")\nasync def publish_candidate_endpoint(\n candidate_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n published_by=str(payload[\"published_by\"]),\n target_channel=str(payload[\"target_channel\"]),\n publication_ref=payload.get(\"publication_ref\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# #endregion publish_candidate_endpoint\n\n\n# #region revoke_publication_endpoint [C:3] [TYPE Function]\n# @BRIEF Endpoint to revoke a previous publication.\n# @RELATION CALLS -> [revoke_publication]\n@router.post(\"/publications/{publication_id}/revoke\")\nasync def revoke_publication_endpoint(\n publication_id: str,\n payload: dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=publication_id,\n revoked_by=str(payload[\"revoked_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc:\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# #endregion revoke_publication_endpoint\n\n# #endregion CleanReleaseV2Api\n" }, { "contract_id": "ApprovalRequest", @@ -11276,7 +11224,7 @@ "complexity": 3, "metadata": { "COMPLEXITY": 3, - "LAYER": "UI (API)", + "LAYER": "API", "PURPOSE": "Defines the FastAPI router for managing external database connections." }, "relations": [ @@ -11293,28 +11241,10 @@ "target_ref": "[ConnectionConfig]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ConnectionsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, connection, api, search]\n# @BRIEF Defines the FastAPI router for managing external database connections.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [get_db]\n# @RELATION DEPENDS_ON -> [ConnectionConfig]\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import ensure_connection_configs_table, get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...models.connection import ConnectionConfig\n\nrouter = APIRouter()\n\n\n# #region _ensure_connections_schema [C:3] [TYPE Function]\n# @BRIEF Ensures the connection_configs table exists before CRUD access.\n# @PRE: db is an active SQLAlchemy session.\n# @POST: The current bind can safely query ConnectionConfig.\n# @RELATION CALLS -> ensure_connection_configs_table\ndef _ensure_connections_schema(db: Session):\n with belief_scope(\"ConnectionsRouter.ensure_schema\"):\n ensure_connection_configs_table(db.get_bind())\n\n\n# #endregion _ensure_connections_schema\n\n\n# #region ConnectionSchema [C:3] [TYPE Class]\n# @BRIEF Pydantic model for connection response.\n# @RELATION BINDS_TO -> ConnectionConfig\nclass ConnectionSchema(BaseModel):\n id: str\n name: str\n type: str\n host: str | None = None\n port: int | None = None\n database: str | None = None\n username: str | None = None\n created_at: datetime\n\n class Config:\n orm_mode = True\n\n\n# #endregion ConnectionSchema\n\n\n# #region ConnectionCreate [C:3] [TYPE Class]\n# @BRIEF Pydantic model for creating a connection.\n# @RELATION BINDS_TO -> ConnectionConfig\nclass ConnectionCreate(BaseModel):\n name: str\n type: str\n host: str | None = None\n port: int | None = None\n database: str | None = None\n username: str | None = None\n password: str | None = None\n\n\n# #endregion ConnectionCreate\n\n\n# #region list_connections [C:3] [TYPE Function]\n# @BRIEF Lists all saved connections.\n# @PRE: Database session is active.\n# @POST: Returns list of connection configs.\n# @RELATION CALLS -> _ensure_connections_schema\n# @RELATION DEPENDS_ON -> ConnectionConfig\n@router.get(\"\", response_model=list[ConnectionSchema])\nasync def list_connections(db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.list_connections\"):\n _ensure_connections_schema(db)\n connections = db.query(ConnectionConfig).all()\n return connections\n\n\n# #endregion list_connections\n\n\n# #region create_connection [C:3] [TYPE Function]\n# @BRIEF Creates a new connection configuration.\n# @PRE: Connection name is unique.\n# @POST: Connection is saved to DB.\n# @RELATION CALLS -> _ensure_connections_schema\n# @RELATION DEPENDS_ON -> ConnectionConfig\n@router.post(\"\", response_model=ConnectionSchema, status_code=status.HTTP_201_CREATED)\nasync def create_connection(\n connection: ConnectionCreate, db: Session = Depends(get_db)\n):\n with belief_scope(\"ConnectionsRouter.create_connection\", f\"name={connection.name}\"):\n _ensure_connections_schema(db)\n db_connection = ConnectionConfig(**connection.dict())\n db.add(db_connection)\n db.commit()\n db.refresh(db_connection)\n logger.info(\n f\"[ConnectionsRouter.create_connection][Success] Created connection {db_connection.id}\"\n )\n return db_connection\n\n\n# #endregion create_connection\n\n\n# #region delete_connection [C:3] [TYPE Function]\n# @BRIEF Deletes a connection configuration.\n# @PRE: Connection ID exists.\n# @POST: Connection is removed from DB.\n# @RELATION CALLS -> _ensure_connections_schema\n# @RELATION DEPENDS_ON -> ConnectionConfig\n@router.delete(\"/{connection_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_connection(connection_id: str, db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.delete_connection\", f\"id={connection_id}\"):\n _ensure_connections_schema(db)\n db_connection = (\n db.query(ConnectionConfig)\n .filter(ConnectionConfig.id == connection_id)\n .first()\n )\n if not db_connection:\n logger.error(\n f\"[ConnectionsRouter.delete_connection][State] Connection {connection_id} not found\"\n )\n raise HTTPException(status_code=404, detail=\"Connection not found\")\n db.delete(db_connection)\n db.commit()\n logger.info(\n f\"[ConnectionsRouter.delete_connection][Success] Deleted connection {connection_id}\"\n )\n return\n\n\n# #endregion delete_connection\n\n# #endregion ConnectionsRouter\n" + "body": "# #region ConnectionsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, connection, api, search]\n# @BRIEF Defines the FastAPI router for managing external database connections.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [get_db]\n# @RELATION DEPENDS_ON -> [ConnectionConfig]\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import ensure_connection_configs_table, get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...models.connection import ConnectionConfig\n\nrouter = APIRouter()\n\n\n# #region _ensure_connections_schema [C:3] [TYPE Function]\n# @BRIEF Ensures the connection_configs table exists before CRUD access.\n# @PRE: db is an active SQLAlchemy session.\n# @POST: The current bind can safely query ConnectionConfig.\n# @RELATION CALLS -> ensure_connection_configs_table\ndef _ensure_connections_schema(db: Session):\n with belief_scope(\"ConnectionsRouter.ensure_schema\"):\n ensure_connection_configs_table(db.get_bind())\n\n\n# #endregion _ensure_connections_schema\n\n\n# #region ConnectionSchema [C:3] [TYPE Class]\n# @BRIEF Pydantic model for connection response.\n# @RELATION BINDS_TO -> ConnectionConfig\nclass ConnectionSchema(BaseModel):\n id: str\n name: str\n type: str\n host: str | None = None\n port: int | None = None\n database: str | None = None\n username: str | None = None\n created_at: datetime\n\n class Config:\n orm_mode = True\n\n\n# #endregion ConnectionSchema\n\n\n# #region ConnectionCreate [C:3] [TYPE Class]\n# @BRIEF Pydantic model for creating a connection.\n# @RELATION BINDS_TO -> ConnectionConfig\nclass ConnectionCreate(BaseModel):\n name: str\n type: str\n host: str | None = None\n port: int | None = None\n database: str | None = None\n username: str | None = None\n password: str | None = None\n\n\n# #endregion ConnectionCreate\n\n\n# #region list_connections [C:3] [TYPE Function]\n# @BRIEF Lists all saved connections.\n# @PRE: Database session is active.\n# @POST: Returns list of connection configs.\n# @RELATION CALLS -> _ensure_connections_schema\n# @RELATION DEPENDS_ON -> ConnectionConfig\n@router.get(\"\", response_model=list[ConnectionSchema])\nasync def list_connections(db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.list_connections\"):\n _ensure_connections_schema(db)\n connections = db.query(ConnectionConfig).all()\n return connections\n\n\n# #endregion list_connections\n\n\n# #region create_connection [C:3] [TYPE Function]\n# @BRIEF Creates a new connection configuration.\n# @PRE: Connection name is unique.\n# @POST: Connection is saved to DB.\n# @RELATION CALLS -> _ensure_connections_schema\n# @RELATION DEPENDS_ON -> ConnectionConfig\n@router.post(\"\", response_model=ConnectionSchema, status_code=status.HTTP_201_CREATED)\nasync def create_connection(\n connection: ConnectionCreate, db: Session = Depends(get_db)\n):\n with belief_scope(\"ConnectionsRouter.create_connection\", f\"name={connection.name}\"):\n _ensure_connections_schema(db)\n db_connection = ConnectionConfig(**connection.dict())\n db.add(db_connection)\n db.commit()\n db.refresh(db_connection)\n logger.info(\n f\"[ConnectionsRouter.create_connection][Success] Created connection {db_connection.id}\"\n )\n return db_connection\n\n\n# #endregion create_connection\n\n\n# #region delete_connection [C:3] [TYPE Function]\n# @BRIEF Deletes a connection configuration.\n# @PRE: Connection ID exists.\n# @POST: Connection is removed from DB.\n# @RELATION CALLS -> _ensure_connections_schema\n# @RELATION DEPENDS_ON -> ConnectionConfig\n@router.delete(\"/{connection_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_connection(connection_id: str, db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.delete_connection\", f\"id={connection_id}\"):\n _ensure_connections_schema(db)\n db_connection = (\n db.query(ConnectionConfig)\n .filter(ConnectionConfig.id == connection_id)\n .first()\n )\n if not db_connection:\n logger.error(\n f\"[ConnectionsRouter.delete_connection][State] Connection {connection_id} not found\"\n )\n raise HTTPException(status_code=404, detail=\"Connection not found\")\n db.delete(db_connection)\n db.commit()\n logger.info(\n f\"[ConnectionsRouter.delete_connection][Success] Deleted connection {connection_id}\"\n )\n return\n\n\n# #endregion delete_connection\n\n# #endregion ConnectionsRouter\n" }, { "contract_id": "_ensure_connections_schema", @@ -15359,14 +15289,18 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/datasets.py", "start_line": 1, - "end_line": 393, - "tier": "TIER_2", - "complexity": 3, + "end_line": 402, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse", "INVARIANT": "All dataset responses include last_task metadata", "LAYER": "API", - "PURPOSE": "API endpoints for the Dataset Hub - listing datasets with mapping progress" + "POST": "Returns dataset metadata with mapping status.", + "PRE": "SupersetClient is available; env_id is valid.", + "PURPOSE": "API endpoints for the Dataset Hub - listing datasets with mapping progress", + "SIDE_EFFECT": "Reads from Superset API and task manager." }, "relations": [ { @@ -15388,27 +15322,17 @@ "target_ref": "[SupersetClient]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetsApi [C:3] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]\n#\n# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [ResourceService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @INVARIANT: All dataset responses include last_task metadata\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_resource_service, get_task_manager, has_permission\n\nrouter = APIRouter(prefix=\"/api/datasets\", tags=[\"Datasets\"])\n\n# #region MappedFields [C:1] [TYPE DataClass]\n# @BRIEF DTO for dataset mapping progress statistics\nclass MappedFields(BaseModel):\n total: int\n mapped: int\n# #endregion MappedFields\n\n# #region LastTask [C:1] [TYPE DataClass]\n# @BRIEF DTO for the most recent task associated with a dataset\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(None, pattern=\"^RUNNING|SUCCESS|ERROR|WAITING_INPUT$\")\n# #endregion LastTask\n\n# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetItem\n\n# #region LinkedDashboard [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dashboard linked to a dataset\nclass LinkedDashboard(BaseModel):\n id: int\n title: str\n slug: str | None = None\n# #endregion LinkedDashboard\n\n# #region DatasetColumn [C:1] [TYPE DataClass]\n# @BRIEF DTO for a single dataset column's metadata\nclass DatasetColumn(BaseModel):\n id: int\n name: str\n type: str | None = None\n is_dttm: bool = False\n is_active: bool = True\n description: str | None = None\n# #endregion DatasetColumn\n\n# #region DatasetDetailResponse [C:1] [TYPE DataClass]\n# @BRIEF Detailed DTO for a dataset including columns and links\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: str | None = None\n schema_name: str | None = Field(None, alias=\"schema\")\n database: str\n description: str | None = None\n columns: list[DatasetColumn]\n column_count: int\n sql: str | None = None\n linked_dashboards: list[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: str | None = None\n changed_on: str | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetDetailResponse\n\n# #region DatasetsResponse [C:1] [TYPE DataClass]\n# @BRIEF Paginated response DTO for dataset listings\nclass DatasetsResponse(BaseModel):\n datasets: list[DatasetItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n# #endregion DatasetsResponse\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF Response DTO containing a task ID for tracking\nclass TaskResponse(BaseModel):\n task_id: str\n# #endregion TaskResponse\n\n# #region get_dataset_ids [C:3] [TYPE Function]\n# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)\n# @PRE: env_id must be a valid environment ID\n# @POST: Returns a list of all dataset IDs\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: str | None = None,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_ids\", f\"env_id={env_id}, search={search}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Extract and return just the IDs\n dataset_ids = [d['id'] for d in datasets]\n logger.info(f\"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs\")\n\n return {\"dataset_ids\": dataset_ids}\n\n except Exception as e:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset IDs: {e!s}\")\n# #endregion get_dataset_ids\n\n# #region get_datasets [C:3] [TYPE Function]\n# @BRIEF Fetch list of datasets from a specific environment with mapping progress\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of datasets with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_datasets\", f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}\"):\n # Validate pagination parameters\n if page < 1:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_datasets][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Calculate pagination\n total = len(datasets)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n\n # Slice datasets for current page\n paginated_datasets = datasets[start_idx:end_idx]\n\n logger.info(f\"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total})\")\n\n return DatasetsResponse(\n datasets=paginated_datasets,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages\n )\n\n except Exception as e:\n logger.error(f\"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch datasets: {e!s}\")\n# #endregion get_datasets\n\n# #region MapColumnsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating column mapping\nclass MapColumnsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to map\")\n source_type: str = Field(..., description=\"Source type: 'postgresql' or 'xlsx'\")\n connection_id: str | None = Field(None, description=\"Connection ID for PostgreSQL source\")\n file_data: str | None = Field(None, description=\"File path or data for XLSX source\")\n# #endregion MapColumnsRequest\n\n# #region map_columns [C:3] [TYPE Function]\n# @BRIEF Trigger bulk column mapping for datasets\n# @PRE: User has permission plugin:mapper:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking mapping progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [MapperPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/map-columns\", response_model=TaskResponse)\nasync def map_columns(\n request: MapColumnsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"map_columns\", f\"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[map_columns][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate source type\n if request.source_type not in ['postgresql', 'xlsx']:\n logger.error(f\"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}\")\n raise HTTPException(status_code=400, detail=\"Source type must be 'postgresql' or 'xlsx'\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[map_columns][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create mapping task\n task_params = {\n 'env': request.env_id,\n 'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,\n 'source': request.source_type,\n 'connection_id': request.connection_id,\n 'file_data': request.file_data\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='dataset-mapper',\n params=task_params\n )\n\n logger.info(f\"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[map_columns][Coherence:Failed] Failed to create mapping task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create mapping task: {e!s}\")\n# #endregion map_columns\n\n# #region GenerateDocsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating documentation generation\nclass GenerateDocsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to generate docs for\")\n llm_provider: str = Field(..., description=\"LLM provider to use\")\n options: dict | None = Field(None, description=\"Additional options for documentation generation\")\n# #endregion GenerateDocsRequest\n\n# #region generate_docs [C:3] [TYPE Function]\n# @BRIEF Trigger bulk documentation generation for datasets\n# @PRE: User has permission plugin:llm_analysis:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking documentation generation progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [DocumentationPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/generate-docs\", response_model=TaskResponse)\nasync def generate_docs(\n request: GenerateDocsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:llm_analysis\", \"EXECUTE\"))\n):\n with belief_scope(\"generate_docs\", f\"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[generate_docs][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create documentation generation task\n task_params = {\n 'environment_id': request.env_id,\n 'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,\n 'provider_id': request.llm_provider,\n 'options': request.options or {}\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='llm_documentation',\n params=task_params\n )\n\n logger.info(f\"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create documentation generation task: {e!s}\")\n# #endregion generate_docs\n\n# #region get_dataset_detail [C:3] [TYPE Function]\n# @BRIEF Get detailed dataset information including columns and linked dashboards\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_id is a valid dataset ID\n# @POST: Returns detailed dataset info with columns and linked dashboards\n# @RELATION CALLS -> [SupersetClientGetDatasetDetail]\n@router.get(\"/{dataset_id}\", response_model=DatasetDetailResponse)\nasync def get_dataset_detail(\n env_id: str,\n dataset_id: int,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_detail\", f\"env_id={env_id}, dataset_id={dataset_id}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Fetch detailed dataset info using SupersetClient\n client = SupersetClient(env)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n logger.info(f\"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards\")\n\n return DatasetDetailResponse(**dataset_detail)\n\n except Exception as e:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset detail: {e!s}\")\n# #endregion get_dataset_detail\n\n# #endregion DatasetsApi\n" + "body": "# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]\n#\n# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [ResourceService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @PRE SupersetClient is available; env_id is valid.\n# @POST Returns dataset metadata with mapping status.\n# @SIDE_EFFECT Reads from Superset API and task manager.\n# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse\n# @INVARIANT: All dataset responses include last_task metadata\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_resource_service, get_task_manager, has_permission\n\nrouter = APIRouter(prefix=\"/api/datasets\", tags=[\"Datasets\"])\n\n# #region MappedFields [C:1] [TYPE DataClass]\n# @BRIEF DTO for dataset mapping progress statistics\nclass MappedFields(BaseModel):\n total: int\n mapped: int\n# #endregion MappedFields\n\n# #region LastTask [C:1] [TYPE DataClass]\n# @BRIEF DTO for the most recent task associated with a dataset\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(None, pattern=\"^RUNNING|SUCCESS|ERROR|WAITING_INPUT$\")\n# #endregion LastTask\n\n# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetItem\n\n# #region LinkedDashboard [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dashboard linked to a dataset\nclass LinkedDashboard(BaseModel):\n id: int\n title: str\n slug: str | None = None\n# #endregion LinkedDashboard\n\n# #region DatasetColumn [C:1] [TYPE DataClass]\n# @BRIEF DTO for a single dataset column's metadata\nclass DatasetColumn(BaseModel):\n id: int\n name: str\n type: str | None = None\n is_dttm: bool = False\n is_active: bool = True\n description: str | None = None\n# #endregion DatasetColumn\n\n# #region DatasetDetailResponse [C:1] [TYPE DataClass]\n# @BRIEF Detailed DTO for a dataset including columns and links\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: str | None = None\n schema_name: str | None = Field(None, alias=\"schema\")\n database: str\n description: str | None = None\n columns: list[DatasetColumn]\n column_count: int\n sql: str | None = None\n linked_dashboards: list[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: str | None = None\n changed_on: str | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetDetailResponse\n\n# #region DatasetsResponse [C:1] [TYPE DataClass]\n# @BRIEF Paginated response DTO for dataset listings\nclass DatasetsResponse(BaseModel):\n datasets: list[DatasetItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n# #endregion DatasetsResponse\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF Response DTO containing a task ID for tracking\nclass TaskResponse(BaseModel):\n task_id: str\n# #endregion TaskResponse\n\n# #region get_dataset_ids [C:4] [TYPE Function]\n# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)\n# @PRE: env_id must be a valid environment ID\n# @POST: Returns a list of all dataset IDs\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: str | None = None,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_ids\", f\"env_id={env_id}, search={search}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Extract and return just the IDs\n dataset_ids = [d['id'] for d in datasets]\n logger.info(f\"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs\")\n\n return {\"dataset_ids\": dataset_ids}\n\n except Exception as e:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset IDs: {e!s}\")\n# #endregion get_dataset_ids\n\n# #region get_datasets [C:4] [TYPE Function]\n# @BRIEF Fetch list of datasets from a specific environment with mapping progress\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of datasets with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_datasets\", f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}\"):\n # Validate pagination parameters\n if page < 1:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_datasets][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Calculate pagination\n total = len(datasets)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n\n # Slice datasets for current page\n paginated_datasets = datasets[start_idx:end_idx]\n\n logger.info(f\"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total})\")\n\n return DatasetsResponse(\n datasets=paginated_datasets,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages\n )\n\n except Exception as e:\n logger.error(f\"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch datasets: {e!s}\")\n# #endregion get_datasets\n\n# #region MapColumnsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating column mapping\nclass MapColumnsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to map\")\n source_type: str = Field(..., description=\"Source type: 'postgresql' or 'xlsx'\")\n connection_id: str | None = Field(None, description=\"Connection ID for PostgreSQL source\")\n file_data: str | None = Field(None, description=\"File path or data for XLSX source\")\n# #endregion MapColumnsRequest\n\n# #region map_columns [C:4] [TYPE Function]\n# @BRIEF Trigger bulk column mapping for datasets\n# @PRE: User has permission plugin:mapper:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking mapping progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [MapperPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/map-columns\", response_model=TaskResponse)\nasync def map_columns(\n request: MapColumnsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"map_columns\", f\"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[map_columns][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate source type\n if request.source_type not in ['postgresql', 'xlsx']:\n logger.error(f\"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}\")\n raise HTTPException(status_code=400, detail=\"Source type must be 'postgresql' or 'xlsx'\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[map_columns][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create mapping task\n task_params = {\n 'env': request.env_id,\n 'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,\n 'source': request.source_type,\n 'connection_id': request.connection_id,\n 'file_data': request.file_data\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='dataset-mapper',\n params=task_params\n )\n\n logger.info(f\"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[map_columns][Coherence:Failed] Failed to create mapping task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create mapping task: {e!s}\")\n# #endregion map_columns\n\n# #region GenerateDocsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating documentation generation\nclass GenerateDocsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to generate docs for\")\n llm_provider: str = Field(..., description=\"LLM provider to use\")\n options: dict | None = Field(None, description=\"Additional options for documentation generation\")\n# #endregion GenerateDocsRequest\n\n# #region generate_docs [C:4] [TYPE Function]\n# @BRIEF Trigger bulk documentation generation for datasets\n# @PRE: User has permission plugin:llm_analysis:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking documentation generation progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [DocumentationPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/generate-docs\", response_model=TaskResponse)\nasync def generate_docs(\n request: GenerateDocsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:llm_analysis\", \"EXECUTE\"))\n):\n with belief_scope(\"generate_docs\", f\"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[generate_docs][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create documentation generation task\n task_params = {\n 'environment_id': request.env_id,\n 'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,\n 'provider_id': request.llm_provider,\n 'options': request.options or {}\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='llm_documentation',\n params=task_params\n )\n\n logger.info(f\"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create documentation generation task: {e!s}\")\n# #endregion generate_docs\n\n# #region get_dataset_detail [C:4] [TYPE Function]\n# @BRIEF Get detailed dataset information including columns and linked dashboards\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_id is a valid dataset ID\n# @POST: Returns detailed dataset info with columns and linked dashboards\n# @RELATION CALLS -> [SupersetClientGetDatasetDetail]\n@router.get(\"/{dataset_id}\", response_model=DatasetDetailResponse)\nasync def get_dataset_detail(\n env_id: str,\n dataset_id: int,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_detail\", f\"env_id={env_id}, dataset_id={dataset_id}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Fetch detailed dataset info using SupersetClient\n client = SupersetClient(env)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Normalize 'database' field: Superset returns an object, model expects a string\n if isinstance(dataset_detail.get(\"database\"), dict):\n db_obj = dataset_detail[\"database\"]\n dataset_detail[\"database\"] = db_obj.get(\"database_name\", str(db_obj.get(\"id\", \"\")))\n\n logger.info(f\"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards\")\n\n return DatasetDetailResponse(**dataset_detail)\n\n except Exception as e:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset detail: {e!s}\")\n# #endregion get_dataset_detail\n\n# #endregion DatasetsApi\n" }, { "contract_id": "MappedFields", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 21, - "end_line": 26, + "start_line": 25, + "end_line": 30, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15479,8 +15403,8 @@ "contract_id": "LastTask", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 28, - "end_line": 33, + "start_line": 32, + "end_line": 37, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15551,8 +15475,8 @@ "contract_id": "DatasetItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 35, - "end_line": 47, + "start_line": 39, + "end_line": 51, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15623,8 +15547,8 @@ "contract_id": "LinkedDashboard", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 49, - "end_line": 55, + "start_line": 53, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15695,8 +15619,8 @@ "contract_id": "DatasetColumn", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 57, - "end_line": 66, + "start_line": 61, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15767,8 +15691,8 @@ "contract_id": "DatasetDetailResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 68, - "end_line": 87, + "start_line": 72, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15839,8 +15763,8 @@ "contract_id": "DatasetsResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 89, - "end_line": 97, + "start_line": 93, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15911,8 +15835,8 @@ "contract_id": "TaskResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 99, - "end_line": 103, + "start_line": 103, + "end_line": 107, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -15983,12 +15907,12 @@ "contract_id": "get_dataset_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 105, - "end_line": 151, + "start_line": 109, + "end_line": 155, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Returns a list of all dataset IDs", "PRE": "env_id must be a valid environment ID", "PURPOSE": "Fetch list of all dataset IDs from a specific environment (without pagination)" @@ -16003,38 +15927,29 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_dataset_ids [C:3] [TYPE Function]\n# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)\n# @PRE: env_id must be a valid environment ID\n# @POST: Returns a list of all dataset IDs\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: str | None = None,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_ids\", f\"env_id={env_id}, search={search}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Extract and return just the IDs\n dataset_ids = [d['id'] for d in datasets]\n logger.info(f\"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs\")\n\n return {\"dataset_ids\": dataset_ids}\n\n except Exception as e:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset IDs: {e!s}\")\n# #endregion get_dataset_ids\n" + "body": "# #region get_dataset_ids [C:4] [TYPE Function]\n# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)\n# @PRE: env_id must be a valid environment ID\n# @POST: Returns a list of all dataset IDs\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: str | None = None,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_ids\", f\"env_id={env_id}, search={search}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Extract and return just the IDs\n dataset_ids = [d['id'] for d in datasets]\n logger.info(f\"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs\")\n\n return {\"dataset_ids\": dataset_ids}\n\n except Exception as e:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset IDs: {e!s}\")\n# #endregion get_dataset_ids\n" }, { "contract_id": "get_datasets", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 153, - "end_line": 225, + "start_line": 157, + "end_line": 229, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Response includes pagination metadata (page, page_size, total, total_pages)", "PRE": "page_size must be between 1 and 100 if provided", "PURPOSE": "Fetch list of datasets from a specific environment with mapping progress" @@ -16049,34 +15964,25 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_datasets [C:3] [TYPE Function]\n# @BRIEF Fetch list of datasets from a specific environment with mapping progress\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of datasets with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_datasets\", f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}\"):\n # Validate pagination parameters\n if page < 1:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_datasets][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Calculate pagination\n total = len(datasets)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n\n # Slice datasets for current page\n paginated_datasets = datasets[start_idx:end_idx]\n\n logger.info(f\"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total})\")\n\n return DatasetsResponse(\n datasets=paginated_datasets,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages\n )\n\n except Exception as e:\n logger.error(f\"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch datasets: {e!s}\")\n# #endregion get_datasets\n" + "body": "# #region get_datasets [C:4] [TYPE Function]\n# @BRIEF Fetch list of datasets from a specific environment with mapping progress\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of datasets with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @RELATION CALLS -> [get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_datasets\", f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}\"):\n # Validate pagination parameters\n if page < 1:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_datasets][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Calculate pagination\n total = len(datasets)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n\n # Slice datasets for current page\n paginated_datasets = datasets[start_idx:end_idx]\n\n logger.info(f\"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total})\")\n\n return DatasetsResponse(\n datasets=paginated_datasets,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages\n )\n\n except Exception as e:\n logger.error(f\"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch datasets: {e!s}\")\n# #endregion get_datasets\n" }, { "contract_id": "MapColumnsRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 227, - "end_line": 235, + "start_line": 231, + "end_line": 239, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16147,12 +16053,12 @@ "contract_id": "map_columns", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 237, - "end_line": 294, + "start_line": 241, + "end_line": 298, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Task is created and queued for execution", "PRE": "dataset_ids is a non-empty list", "PURPOSE": "Trigger bulk column mapping for datasets" @@ -16173,34 +16079,25 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region map_columns [C:3] [TYPE Function]\n# @BRIEF Trigger bulk column mapping for datasets\n# @PRE: User has permission plugin:mapper:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking mapping progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [MapperPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/map-columns\", response_model=TaskResponse)\nasync def map_columns(\n request: MapColumnsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"map_columns\", f\"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[map_columns][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate source type\n if request.source_type not in ['postgresql', 'xlsx']:\n logger.error(f\"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}\")\n raise HTTPException(status_code=400, detail=\"Source type must be 'postgresql' or 'xlsx'\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[map_columns][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create mapping task\n task_params = {\n 'env': request.env_id,\n 'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,\n 'source': request.source_type,\n 'connection_id': request.connection_id,\n 'file_data': request.file_data\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='dataset-mapper',\n params=task_params\n )\n\n logger.info(f\"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[map_columns][Coherence:Failed] Failed to create mapping task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create mapping task: {e!s}\")\n# #endregion map_columns\n" + "body": "# #region map_columns [C:4] [TYPE Function]\n# @BRIEF Trigger bulk column mapping for datasets\n# @PRE: User has permission plugin:mapper:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking mapping progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [MapperPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/map-columns\", response_model=TaskResponse)\nasync def map_columns(\n request: MapColumnsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"map_columns\", f\"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[map_columns][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate source type\n if request.source_type not in ['postgresql', 'xlsx']:\n logger.error(f\"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}\")\n raise HTTPException(status_code=400, detail=\"Source type must be 'postgresql' or 'xlsx'\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[map_columns][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create mapping task\n task_params = {\n 'env': request.env_id,\n 'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,\n 'source': request.source_type,\n 'connection_id': request.connection_id,\n 'file_data': request.file_data\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='dataset-mapper',\n params=task_params\n )\n\n logger.info(f\"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[map_columns][Coherence:Failed] Failed to create mapping task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create mapping task: {e!s}\")\n# #endregion map_columns\n" }, { "contract_id": "GenerateDocsRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 296, - "end_line": 303, + "start_line": 300, + "end_line": 307, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16271,12 +16168,12 @@ "contract_id": "generate_docs", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 305, - "end_line": 356, + "start_line": 309, + "end_line": 360, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Task is created and queued for execution", "PRE": "dataset_ids is a non-empty list", "PURPOSE": "Trigger bulk documentation generation for datasets" @@ -16297,38 +16194,29 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region generate_docs [C:3] [TYPE Function]\n# @BRIEF Trigger bulk documentation generation for datasets\n# @PRE: User has permission plugin:llm_analysis:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking documentation generation progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [DocumentationPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/generate-docs\", response_model=TaskResponse)\nasync def generate_docs(\n request: GenerateDocsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:llm_analysis\", \"EXECUTE\"))\n):\n with belief_scope(\"generate_docs\", f\"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[generate_docs][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create documentation generation task\n task_params = {\n 'environment_id': request.env_id,\n 'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,\n 'provider_id': request.llm_provider,\n 'options': request.options or {}\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='llm_documentation',\n params=task_params\n )\n\n logger.info(f\"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create documentation generation task: {e!s}\")\n# #endregion generate_docs\n" + "body": "# #region generate_docs [C:4] [TYPE Function]\n# @BRIEF Trigger bulk documentation generation for datasets\n# @PRE: User has permission plugin:llm_analysis:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_ids is a non-empty list\n# @POST: Returns task_id for tracking documentation generation progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [DocumentationPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/generate-docs\", response_model=TaskResponse)\nasync def generate_docs(\n request: GenerateDocsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:llm_analysis\", \"EXECUTE\"))\n):\n with belief_scope(\"generate_docs\", f\"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[generate_docs][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create documentation generation task\n task_params = {\n 'environment_id': request.env_id,\n 'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,\n 'provider_id': request.llm_provider,\n 'options': request.options or {}\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='llm_documentation',\n params=task_params\n )\n\n logger.info(f\"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create documentation generation task: {e!s}\")\n# #endregion generate_docs\n" }, { "contract_id": "get_dataset_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 358, - "end_line": 391, + "start_line": 362, + "end_line": 400, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "POST": "Returns detailed dataset info with columns and linked dashboards", "PRE": "dataset_id is a valid dataset ID", "PURPOSE": "Get detailed dataset information including columns and linked dashboards" @@ -16343,27 +16231,18 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, + "actual_complexity": 4, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_dataset_detail [C:3] [TYPE Function]\n# @BRIEF Get detailed dataset information including columns and linked dashboards\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_id is a valid dataset ID\n# @POST: Returns detailed dataset info with columns and linked dashboards\n# @RELATION CALLS -> [SupersetClientGetDatasetDetail]\n@router.get(\"/{dataset_id}\", response_model=DatasetDetailResponse)\nasync def get_dataset_detail(\n env_id: str,\n dataset_id: int,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_detail\", f\"env_id={env_id}, dataset_id={dataset_id}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Fetch detailed dataset info using SupersetClient\n client = SupersetClient(env)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n logger.info(f\"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards\")\n\n return DatasetDetailResponse(**dataset_detail)\n\n except Exception as e:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset detail: {e!s}\")\n# #endregion get_dataset_detail\n" + "body": "# #region get_dataset_detail [C:4] [TYPE Function]\n# @BRIEF Get detailed dataset information including columns and linked dashboards\n# @PRE: env_id is a valid environment ID\n# @PRE: dataset_id is a valid dataset ID\n# @POST: Returns detailed dataset info with columns and linked dashboards\n# @RELATION CALLS -> [SupersetClientGetDatasetDetail]\n@router.get(\"/{dataset_id}\", response_model=DatasetDetailResponse)\nasync def get_dataset_detail(\n env_id: str,\n dataset_id: int,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_detail\", f\"env_id={env_id}, dataset_id={dataset_id}\"):\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Fetch detailed dataset info using SupersetClient\n client = SupersetClient(env)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Normalize 'database' field: Superset returns an object, model expects a string\n if isinstance(dataset_detail.get(\"database\"), dict):\n db_obj = dataset_detail[\"database\"]\n dataset_detail[\"database\"] = db_obj.get(\"database_name\", str(db_obj.get(\"id\", \"\")))\n\n logger.info(f\"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards\")\n\n return DatasetDetailResponse(**dataset_detail)\n\n except Exception as e:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset detail: {e!s}\")\n# #endregion get_dataset_detail\n" }, { "contract_id": "EnvironmentsApi", @@ -16371,10 +16250,10 @@ "file_path": "backend/src/api/routes/environments.py", "start_line": 1, "end_line": 148, - "tier": "TIER_2", - "complexity": 3, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "INVARIANT": "Environment IDs must exist in the configuration.", "LAYER": "API", "PURPOSE": "API endpoints for listing environments and their databases." @@ -16395,18 +16274,45 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region EnvironmentsApi [C:3] [TYPE Module] [SEMANTICS fastapi, environment, api]\n#\n# @BRIEF API endpoints for listing environments and their databases.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @INVARIANT: Environment IDs must exist in the configuration.\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_scheduler_service, has_permission\n\nrouter = APIRouter(prefix=\"/api/environments\", tags=[\"Environments\"])\n\n\n# #region _normalize_superset_env_url [TYPE Function]\n# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.\n# @PRE: raw_url can be empty.\n# @POST: Returns normalized base URL.\ndef _normalize_superset_env_url(raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n return normalized.rstrip(\"/\")\n# #endregion _normalize_superset_env_url\n\n# #region ScheduleSchema [TYPE DataClass]\nclass ScheduleSchema(BaseModel):\n enabled: bool = False\n cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\\d+,)*\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){4,6})$')\n# #endregion ScheduleSchema\n\n# #region EnvironmentResponse [TYPE DataClass]\nclass EnvironmentResponse(BaseModel):\n id: str\n name: str\n url: str\n stage: str = \"DEV\"\n is_production: bool = False\n backup_schedule: ScheduleSchema | None = None\n# #endregion EnvironmentResponse\n\n# #region DatabaseResponse [TYPE DataClass]\nclass DatabaseResponse(BaseModel):\n uuid: str\n database_name: str\n engine: str | None\n# #endregion DatabaseResponse\n\n# #region get_environments [TYPE Function] [SEMANTICS list, environments, config]\n# @BRIEF List all configured environments.\n# @LAYER: API\n# @PRE: config_manager is injected via Depends.\n# @POST: Returns a list of EnvironmentResponse objects.\n@router.get(\"\", response_model=list[EnvironmentResponse])\nasync def get_environments(\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"environments\", \"READ\"))\n):\n with belief_scope(\"get_environments\"):\n envs = config_manager.get_environments()\n # Ensure envs is a list\n if not isinstance(envs, list):\n envs = []\n response_items = []\n for e in envs:\n resolved_stage = str(\n getattr(e, \"stage\", \"\")\n or (\"PROD\" if bool(getattr(e, \"is_production\", False)) else \"DEV\")\n ).upper()\n response_items.append(\n EnvironmentResponse(\n id=e.id,\n name=e.name,\n url=_normalize_superset_env_url(e.url),\n stage=resolved_stage,\n is_production=(resolved_stage == \"PROD\"),\n backup_schedule=ScheduleSchema(\n enabled=e.backup_schedule.enabled,\n cron_expression=e.backup_schedule.cron_expression\n ) if getattr(e, 'backup_schedule', None) else None\n )\n )\n return response_items\n# #endregion get_environments\n\n# #region update_environment_schedule [TYPE Function] [SEMANTICS update, schedule, backup, environment]\n# @BRIEF Update backup schedule for an environment.\n# @LAYER: API\n# @PRE: Environment id exists, schedule is valid ScheduleSchema.\n# @POST: Backup schedule updated and scheduler reloaded.\n@router.put(\"/{id}/schedule\")\nasync def update_environment_schedule(\n id: str,\n schedule: ScheduleSchema,\n config_manager=Depends(get_config_manager),\n scheduler_service=Depends(get_scheduler_service),\n _ = Depends(has_permission(\"admin:settings\", \"WRITE\"))\n):\n with belief_scope(\"update_environment_schedule\", f\"id={id}\"):\n envs = config_manager.get_environments()\n env = next((e for e in envs if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n # Update environment config\n env.backup_schedule.enabled = schedule.enabled\n env.backup_schedule.cron_expression = schedule.cron_expression\n\n config_manager.update_environment(id, env)\n\n # Refresh scheduler\n scheduler_service.load_schedules()\n\n return {\"message\": \"Schedule updated successfully\"}\n# #endregion update_environment_schedule\n\n# #region get_environment_databases [TYPE Function] [SEMANTICS fetch, databases, superset, environment]\n# @BRIEF Fetch the list of databases from a specific environment.\n# @LAYER: API\n# @PRE: Environment id exists.\n# @POST: Returns a list of database summaries from the environment.\n@router.get(\"/{id}/databases\")\nasync def get_environment_databases(\n id: str,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"admin:settings\", \"READ\"))\n):\n with belief_scope(\"get_environment_databases\", f\"id={id}\"):\n envs = config_manager.get_environments()\n env = next((e for e in envs if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Initialize SupersetClient from environment config\n client = SupersetClient(env)\n return client.get_databases_summary()\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"Failed to fetch databases: {e!s}\")\n# #endregion get_environment_databases\n\n# #endregion EnvironmentsApi\n" + "body": "# #region EnvironmentsApi [C:5] [TYPE Module] [SEMANTICS fastapi, environment, api]\n#\n# @BRIEF API endpoints for listing environments and their databases.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @INVARIANT: Environment IDs must exist in the configuration.\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_scheduler_service, has_permission\n\nrouter = APIRouter(prefix=\"/api/environments\", tags=[\"Environments\"])\n\n\n# #region _normalize_superset_env_url [TYPE Function]\n# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.\n# @PRE: raw_url can be empty.\n# @POST: Returns normalized base URL.\ndef _normalize_superset_env_url(raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n return normalized.rstrip(\"/\")\n# #endregion _normalize_superset_env_url\n\n# #region ScheduleSchema [TYPE DataClass]\nclass ScheduleSchema(BaseModel):\n enabled: bool = False\n cron_expression: str = Field(..., pattern=r'^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|((((\\d+,)*\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){4,6})$')\n# #endregion ScheduleSchema\n\n# #region EnvironmentResponse [TYPE DataClass]\nclass EnvironmentResponse(BaseModel):\n id: str\n name: str\n url: str\n stage: str = \"DEV\"\n is_production: bool = False\n backup_schedule: ScheduleSchema | None = None\n# #endregion EnvironmentResponse\n\n# #region DatabaseResponse [TYPE DataClass]\nclass DatabaseResponse(BaseModel):\n uuid: str\n database_name: str\n engine: str | None\n# #endregion DatabaseResponse\n\n# #region get_environments [TYPE Function] [SEMANTICS list, environments, config]\n# @BRIEF List all configured environments.\n# @LAYER: API\n# @PRE: config_manager is injected via Depends.\n# @POST: Returns a list of EnvironmentResponse objects.\n@router.get(\"\", response_model=list[EnvironmentResponse])\nasync def get_environments(\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"environments\", \"READ\"))\n):\n with belief_scope(\"get_environments\"):\n envs = config_manager.get_environments()\n # Ensure envs is a list\n if not isinstance(envs, list):\n envs = []\n response_items = []\n for e in envs:\n resolved_stage = str(\n getattr(e, \"stage\", \"\")\n or (\"PROD\" if bool(getattr(e, \"is_production\", False)) else \"DEV\")\n ).upper()\n response_items.append(\n EnvironmentResponse(\n id=e.id,\n name=e.name,\n url=_normalize_superset_env_url(e.url),\n stage=resolved_stage,\n is_production=(resolved_stage == \"PROD\"),\n backup_schedule=ScheduleSchema(\n enabled=e.backup_schedule.enabled,\n cron_expression=e.backup_schedule.cron_expression\n ) if getattr(e, 'backup_schedule', None) else None\n )\n )\n return response_items\n# #endregion get_environments\n\n# #region update_environment_schedule [TYPE Function] [SEMANTICS update, schedule, backup, environment]\n# @BRIEF Update backup schedule for an environment.\n# @LAYER: API\n# @PRE: Environment id exists, schedule is valid ScheduleSchema.\n# @POST: Backup schedule updated and scheduler reloaded.\n@router.put(\"/{id}/schedule\")\nasync def update_environment_schedule(\n id: str,\n schedule: ScheduleSchema,\n config_manager=Depends(get_config_manager),\n scheduler_service=Depends(get_scheduler_service),\n _ = Depends(has_permission(\"admin:settings\", \"WRITE\"))\n):\n with belief_scope(\"update_environment_schedule\", f\"id={id}\"):\n envs = config_manager.get_environments()\n env = next((e for e in envs if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n # Update environment config\n env.backup_schedule.enabled = schedule.enabled\n env.backup_schedule.cron_expression = schedule.cron_expression\n\n config_manager.update_environment(id, env)\n\n # Refresh scheduler\n scheduler_service.load_schedules()\n\n return {\"message\": \"Schedule updated successfully\"}\n# #endregion update_environment_schedule\n\n# #region get_environment_databases [TYPE Function] [SEMANTICS fetch, databases, superset, environment]\n# @BRIEF Fetch the list of databases from a specific environment.\n# @LAYER: API\n# @PRE: Environment id exists.\n# @POST: Returns a list of database summaries from the environment.\n@router.get(\"/{id}/databases\")\nasync def get_environment_databases(\n id: str,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"admin:settings\", \"READ\"))\n):\n with belief_scope(\"get_environment_databases\", f\"id={id}\"):\n envs = config_manager.get_environments()\n env = next((e for e in envs if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Initialize SupersetClient from environment config\n client = SupersetClient(env)\n return client.get_databases_summary()\n except Exception as e:\n raise HTTPException(status_code=500, detail=f\"Failed to fetch databases: {e!s}\")\n# #endregion get_environment_databases\n\n# #endregion EnvironmentsApi\n" }, { "contract_id": "_normalize_superset_env_url", @@ -16618,11 +16524,11 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/git/__init__.py", "start_line": 1, - "end_line": 42, - "tier": "TIER_2", - "complexity": 3, + "end_line": 46, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "LAYER": "API", "PURPOSE": "Package root for decomposed git routes. Re-exports all public symbols from submodules." }, @@ -16635,6 +16541,51 @@ } ], "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -16656,7 +16607,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitPackage [C:3] [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 USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes,\n# GitRepoRoutes, GitRepoOperationsRoutes, GitRepoLifecycleRoutes,\n# GitMergeRoutes, GitEnvironmentRoutes]\n# @INVARIANT: git_service and os are module-level attributes for test monkeypatch compatibility.\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 USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes,\n# GitRepoRoutes, GitRepoOperationsRoutes, GitRepoLifecycleRoutes,\n# GitMergeRoutes, GitEnvironmentRoutes]\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", @@ -17461,7 +17412,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, history, diff]\n# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n CommitCreate,\n CommitSchema,\n RepoStatusBatchRequest,\n RepoStatusBatchResponse,\n)\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.git import GitRepository, GitServerConfig\n\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH, get_git_service\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _build_no_repo_status_payload,\n _handle_unexpected_git_route_error,\n _resolve_repository_status,\n)\nfrom ._router import router\n\n\n# #region commit_changes [C:2] [TYPE Function]\n# @BRIEF Stage and commit changes in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/commit\")\nasync def commit_changes(\n dashboard_ref: str,\n commit_data: CommitCreate,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"commit_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.commit_changes(\n dashboard_id, commit_data.message, commit_data.files\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"commit_changes\", e)\n# #endregion commit_changes\n\n\n# #region push_changes [C:2] [TYPE Function]\n# @BRIEF Push local commits to the remote repository.\n@router.post(\"/repositories/{dashboard_ref}/push\")\nasync def push_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"push_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _gs.push_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"push_changes\", e)\n# #endregion push_changes\n\n\n# #region pull_changes [C:3] [TYPE Function]\n# @BRIEF Pull changes from the remote repository.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/pull\")\nasync def pull_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"pull_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = None\n config_url = None\n config_provider = None\n try:\n db_repo_candidate = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if getattr(db_repo_candidate, \"config_id\", None):\n db_repo = db_repo_candidate\n config_row = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == db_repo.config_id)\n .first()\n )\n if config_row:\n config_url = config_row.url\n config_provider = config_row.provider\n except Exception as diagnostics_error:\n logger.explore(\n \"Failed to load repository binding diagnostics\",\n extra={\"src\": \"pull_changes\", \"payload\": {\"dashboard_id\": dashboard_id}, \"error\": str(diagnostics_error)},\n )\n logger.reason(\n f\"Route diagnostics dashboard_ref={dashboard_ref} env_id={env_id} resolved_dashboard_id={dashboard_id} \"\n f\"binding_exists={bool(db_repo)} binding_local_path={(db_repo.local_path if db_repo else None)} \"\n f\"binding_remote_url={(db_repo.remote_url if db_repo else None)} \"\n f\"binding_config_id={(db_repo.config_id if db_repo else None)} \"\n f\"config_provider={config_provider} config_url={config_url}\",\n extra={\"src\": \"pull_changes\"},\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.pull_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"pull_changes\", e)\n# #endregion pull_changes\n\n\n# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref_async\n\n try:\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n\n\n# #region get_repository_status_batch [C:2] [TYPE Function]\n# @BRIEF Get Git statuses for multiple dashboard repositories in one request.\n@router.post(\"/repositories/status/batch\", response_model=RepoStatusBatchResponse)\nasync def get_repository_status_batch(\n request: RepoStatusBatchRequest,\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status_batch\"):\n dashboard_ids = list(dict.fromkeys(request.dashboard_ids))\n if len(dashboard_ids) > MAX_REPOSITORY_STATUS_BATCH:\n logger.reason(\n f\"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.\",\n extra={\"src\": \"get_repository_status_batch\"},\n )\n dashboard_ids = dashboard_ids[:MAX_REPOSITORY_STATUS_BATCH]\n\n statuses = {}\n for dashboard_id in dashboard_ids:\n try:\n statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id)\n except HTTPException:\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n except Exception as e:\n logger.error(\n f\"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}\"\n )\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n return RepoStatusBatchResponse(statuses=statuses)\n# #endregion get_repository_status_batch\n\n\n# #region get_repository_diff [C:2] [TYPE Function]\n# @BRIEF Get Git diff for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/diff\")\nasync def get_repository_diff(\n dashboard_ref: str,\n file_path: str | None = None,\n staged: bool = False,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_repository_diff\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_diff(dashboard_id, file_path, staged)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_diff\", e)\n# #endregion get_repository_diff\n\n\n# #region get_history [C:2] [TYPE Function]\n# @BRIEF View commit history for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/history\", response_model=list[CommitSchema])\nasync def get_history(\n dashboard_ref: str,\n limit: int = 50,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_history\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_commit_history(dashboard_id, limit)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_history\", e)\n# #endregion get_history\n\n\n# #region generate_commit_message [C:3] [TYPE Function]\n# @BRIEF Generate a suggested commit message using LLM.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/generate-message\")\nasync def generate_commit_message(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"generate_commit_message\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n diff = _gs.get_diff(dashboard_id, staged=True)\n if not diff:\n diff = _gs.get_diff(dashboard_id, staged=False)\n\n if not diff:\n return {\"message\": \"No changes detected\"}\n\n history_objs = _gs.get_commit_history(dashboard_id, limit=5)\n history = [h.message for h in history_objs if hasattr(h, \"message\")]\n\n from ...plugins.llm_analysis.models import LLMProviderType\n from ...plugins.llm_analysis.service import LLMClient\n from ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\n from ...services.llm_provider import LLMProviderService\n\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n bound_provider_id = resolve_bound_provider_id(llm_settings, \"git_commit\")\n provider = next((p for p in providers if p.id == bound_provider_id), None)\n if not provider:\n provider = next((p for p in providers if p.is_active), None)\n\n if not provider:\n raise HTTPException(\n status_code=400, detail=\"No active LLM provider found\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider.id)\n client = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=provider.default_model,\n )\n\n from ...plugins.git.llm_extension import GitLLMExtension\n\n extension = GitLLMExtension(client)\n git_prompt = llm_settings[\"prompts\"].get(\n \"git_commit_prompt\",\n DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n )\n message = await extension.suggest_commit_message(\n diff,\n history,\n prompt_template=git_prompt,\n )\n return {\"message\": message}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"generate_commit_message\", e)\n# #endregion generate_commit_message\n# #endregion GitRepoOperationsRoutes\n" + "body": "# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, history, diff]\n# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n CommitCreate,\n CommitSchema,\n RepoStatusBatchRequest,\n RepoStatusBatchResponse,\n)\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.git import GitRepository, GitServerConfig\n\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH, get_git_service\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _build_no_repo_status_payload,\n _handle_unexpected_git_route_error,\n _resolve_repository_status,\n)\nfrom ._router import router\n\n\n# #region commit_changes [C:2] [TYPE Function]\n# @BRIEF Stage and commit changes in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/commit\")\nasync def commit_changes(\n dashboard_ref: str,\n commit_data: CommitCreate,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"commit_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.commit_changes(\n dashboard_id, commit_data.message, commit_data.files\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"commit_changes\", e)\n# #endregion commit_changes\n\n\n# #region push_changes [C:2] [TYPE Function]\n# @BRIEF Push local commits to the remote repository.\n@router.post(\"/repositories/{dashboard_ref}/push\")\nasync def push_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"push_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _gs.push_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"push_changes\", e)\n# #endregion push_changes\n\n\n# #region pull_changes [C:3] [TYPE Function]\n# @BRIEF Pull changes from the remote repository.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/pull\")\nasync def pull_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"pull_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = None\n config_url = None\n config_provider = None\n try:\n db_repo_candidate = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if getattr(db_repo_candidate, \"config_id\", None):\n db_repo = db_repo_candidate\n config_row = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == db_repo.config_id)\n .first()\n )\n if config_row:\n config_url = config_row.url\n config_provider = config_row.provider\n except Exception as diagnostics_error:\n logger.explore(\n \"Failed to load repository binding diagnostics\",\n extra={\"src\": \"pull_changes\", \"payload\": {\"dashboard_id\": dashboard_id}, \"error\": str(diagnostics_error)},\n )\n logger.reason(\n f\"Route diagnostics dashboard_ref={dashboard_ref} env_id={env_id} resolved_dashboard_id={dashboard_id} \"\n f\"binding_exists={bool(db_repo)} binding_local_path={(db_repo.local_path if db_repo else None)} \"\n f\"binding_remote_url={(db_repo.remote_url if db_repo else None)} \"\n f\"binding_config_id={(db_repo.config_id if db_repo else None)} \"\n f\"config_provider={config_provider} config_url={config_url}\",\n extra={\"src\": \"pull_changes\"},\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.pull_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"pull_changes\", e)\n# #endregion pull_changes\n\n\n# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref_async\n\n try:\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n\n\n# #region get_repository_status_batch [C:2] [TYPE Function]\n# @BRIEF Get Git statuses for multiple dashboard repositories in one request.\n@router.post(\"/repositories/status/batch\", response_model=RepoStatusBatchResponse)\nasync def get_repository_status_batch(\n request: RepoStatusBatchRequest,\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status_batch\"):\n dashboard_ids = list(dict.fromkeys(request.dashboard_ids))\n if len(dashboard_ids) > MAX_REPOSITORY_STATUS_BATCH:\n logger.reason(\n f\"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.\",\n extra={\"src\": \"get_repository_status_batch\"},\n )\n dashboard_ids = dashboard_ids[:MAX_REPOSITORY_STATUS_BATCH]\n\n statuses = {}\n for dashboard_id in dashboard_ids:\n try:\n statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id)\n except HTTPException:\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n except Exception as e:\n logger.error(\n f\"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}\"\n )\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n return RepoStatusBatchResponse(statuses=statuses)\n# #endregion get_repository_status_batch\n\n\n# #region get_repository_diff [C:2] [TYPE Function]\n# @BRIEF Get Git diff for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/diff\")\nasync def get_repository_diff(\n dashboard_ref: str,\n file_path: str | None = None,\n staged: bool = False,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_repository_diff\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_diff(dashboard_id, file_path, staged)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_diff\", e)\n# #endregion get_repository_diff\n\n\n# #region get_history [C:2] [TYPE Function]\n# @BRIEF View commit history for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/history\", response_model=list[CommitSchema])\nasync def get_history(\n dashboard_ref: str,\n limit: int = 50,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_history\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_commit_history(dashboard_id, limit)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_history\", e)\n# #endregion get_history\n\n\n# #region generate_commit_message [C:3] [TYPE Function]\n# @BRIEF Generate a suggested commit message using LLM.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/generate-message\")\nasync def generate_commit_message(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"generate_commit_message\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n diff = _gs.get_diff(dashboard_id, staged=True)\n if not diff:\n diff = _gs.get_diff(dashboard_id, staged=False)\n\n if not diff:\n return {\"message\": \"No changes detected\"}\n\n history_objs = _gs.get_commit_history(dashboard_id, limit=5)\n history = [h.message for h in history_objs if hasattr(h, \"message\")]\n\n from src.plugins.llm_analysis.models import LLMProviderType\n from src.plugins.llm_analysis.service import LLMClient\n from src.services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\n from src.services.llm_provider import LLMProviderService\n\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n bound_provider_id = resolve_bound_provider_id(llm_settings, \"git_commit\")\n provider = next((p for p in providers if p.id == bound_provider_id), None)\n if not provider:\n provider = next((p for p in providers if p.is_active), None)\n\n if not provider:\n raise HTTPException(\n status_code=400, detail=\"No active LLM provider found\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider.id)\n client = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=provider.default_model,\n )\n\n from src.plugins.git.llm_extension import GitLLMExtension\n\n extension = GitLLMExtension(client)\n git_prompt = llm_settings[\"prompts\"].get(\n \"git_commit_prompt\",\n DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n )\n message = await extension.suggest_commit_message(\n diff,\n history,\n prompt_template=git_prompt,\n )\n return {\"message\": message}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"generate_commit_message\", e)\n# #endregion generate_commit_message\n# #endregion GitRepoOperationsRoutes\n" }, { "contract_id": "commit_changes", @@ -17601,7 +17552,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region generate_commit_message [C:3] [TYPE Function]\n# @BRIEF Generate a suggested commit message using LLM.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/generate-message\")\nasync def generate_commit_message(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"generate_commit_message\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n diff = _gs.get_diff(dashboard_id, staged=True)\n if not diff:\n diff = _gs.get_diff(dashboard_id, staged=False)\n\n if not diff:\n return {\"message\": \"No changes detected\"}\n\n history_objs = _gs.get_commit_history(dashboard_id, limit=5)\n history = [h.message for h in history_objs if hasattr(h, \"message\")]\n\n from ...plugins.llm_analysis.models import LLMProviderType\n from ...plugins.llm_analysis.service import LLMClient\n from ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\n from ...services.llm_provider import LLMProviderService\n\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n bound_provider_id = resolve_bound_provider_id(llm_settings, \"git_commit\")\n provider = next((p for p in providers if p.id == bound_provider_id), None)\n if not provider:\n provider = next((p for p in providers if p.is_active), None)\n\n if not provider:\n raise HTTPException(\n status_code=400, detail=\"No active LLM provider found\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider.id)\n client = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=provider.default_model,\n )\n\n from ...plugins.git.llm_extension import GitLLMExtension\n\n extension = GitLLMExtension(client)\n git_prompt = llm_settings[\"prompts\"].get(\n \"git_commit_prompt\",\n DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n )\n message = await extension.suggest_commit_message(\n diff,\n history,\n prompt_template=git_prompt,\n )\n return {\"message\": message}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"generate_commit_message\", e)\n# #endregion generate_commit_message\n" + "body": "# #region generate_commit_message [C:3] [TYPE Function]\n# @BRIEF Generate a suggested commit message using LLM.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/generate-message\")\nasync def generate_commit_message(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"generate_commit_message\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n diff = _gs.get_diff(dashboard_id, staged=True)\n if not diff:\n diff = _gs.get_diff(dashboard_id, staged=False)\n\n if not diff:\n return {\"message\": \"No changes detected\"}\n\n history_objs = _gs.get_commit_history(dashboard_id, limit=5)\n history = [h.message for h in history_objs if hasattr(h, \"message\")]\n\n from src.plugins.llm_analysis.models import LLMProviderType\n from src.plugins.llm_analysis.service import LLMClient\n from src.services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\n from src.services.llm_provider import LLMProviderService\n\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n bound_provider_id = resolve_bound_provider_id(llm_settings, \"git_commit\")\n provider = next((p for p in providers if p.id == bound_provider_id), None)\n if not provider:\n provider = next((p for p in providers if p.is_active), None)\n\n if not provider:\n raise HTTPException(\n status_code=400, detail=\"No active LLM provider found\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider.id)\n client = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=provider.default_model,\n )\n\n from src.plugins.git.llm_extension import GitLLMExtension\n\n extension = GitLLMExtension(client)\n git_prompt = llm_settings[\"prompts\"].get(\n \"git_commit_prompt\",\n DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n )\n message = await extension.suggest_commit_message(\n diff,\n history,\n prompt_template=git_prompt,\n )\n return {\"message\": message}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"generate_commit_message\", e)\n# #endregion generate_commit_message\n" }, { "contract_id": "GitRepoRoutes", @@ -18429,12 +18380,12 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/llm.py", "start_line": 1, - "end_line": 430, + "end_line": 432, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "LAYER": "UI (API)", + "LAYER": "API", "PURPOSE": "API routes for LLM provider configuration and management." }, "relations": [ @@ -18463,28 +18414,10 @@ "target_ref": "[get_db]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LlmRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, llm, api, provider]\n# @BRIEF API routes for LLM provider configuration and management.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, Field\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import logger\nfrom ...dependencies import get_current_user as get_current_active_user\nfrom ...plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType\nfrom ...schemas.auth import User\nfrom ...services.llm_provider import LLMProviderService, is_masked_or_placeholder, mask_api_key\n\n\n# #region FetchModelsRequest [C:1] [TYPE Class]\n# @BRIEF Pydantic request model for the fetch-models endpoint.\nclass FetchModelsRequest(BaseModel):\n base_url: str | None = Field(None, description=\"LLM provider base URL\")\n provider_type: str | None = Field(None, description=\"Provider type (openai, anthropic, etc)\")\n api_key: str | None = Field(None, description=\"Direct API key (takes precedence over provider_id)\")\n provider_id: str | None = Field(None, description=\"Saved provider ID to look up stored API key\")\n\n\n# #endregion FetchModelsRequest\n\n# #region router [TYPE Global]\n# @BRIEF APIRouter instance for LLM routes.\nrouter = APIRouter(tags=[\"LLM\"])\n# #endregion router\n\n\n# #region _is_valid_runtime_api_key [C:4] [TYPE Function]\n# @BRIEF Validate decrypted runtime API key presence/shape.\n# @PRE: value can be None.\n# @POST: Returns True only for non-placeholder key.\n# @RELATION BINDS_TO -> [LlmRoutes]\ndef _is_valid_runtime_api_key(value: str | None) -> bool:\n key = (value or \"\").strip()\n if not key:\n return False\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return False\n return len(key) >= 16\n\n\n# #endregion _is_valid_runtime_api_key\n\n\n# #region get_providers [C:4] [TYPE Function]\n# @BRIEF Retrieve all LLM provider configurations.\n# @PRE: User is authenticated.\n# @POST: Returns list of LLMProviderConfig.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.get(\"/providers\", response_model=list[LLMProviderConfig])\nasync def get_providers(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n \"\"\"\n Get all LLM provider configurations.\n \"\"\"\n logger.reason(\n f\"Fetching providers for user: {current_user.username}\",\n extra={\"src\": \"llm_routes.get_providers\"},\n )\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n return [\n LLMProviderConfig(\n id=p.id,\n provider_type=LLMProviderType(p.provider_type),\n name=p.name,\n base_url=p.base_url,\n api_key=mask_api_key(service.get_decrypted_api_key(p.id)) if p.api_key else \"\",\n default_model=p.default_model,\n is_active=p.is_active,\n )\n for p in providers\n ]\n\n\n# #endregion get_providers\n\n\n# #region fetch_models [C:4] [TYPE Function]\n# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.\n# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.\n# @POST: Returns a list of available model IDs.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION CALLS -> [LLMClient]\n@router.post(\"/providers/fetch-models\")\nasync def fetch_models(\n payload: FetchModelsRequest,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n from ...plugins.llm_analysis.models import LLMProviderType\n from ...plugins.llm_analysis.service import LLMClient\n\n base_url = (payload.base_url or \"\").strip()\n provider_type_str = (payload.provider_type or \"\").strip()\n api_key = payload.api_key or \"\"\n provider_id = payload.provider_id or \"\"\n\n # Treat masked/placeholder API keys as if no api_key was provided;\n # they are display-only and cannot be used for real API calls.\n if is_masked_or_placeholder(api_key):\n api_key = \"\"\n\n # Early validation: at least one of api_key or provider_id is required\n if not api_key and not provider_id:\n raise HTTPException(\n status_code=400,\n detail=\"api_key or provider_id is required\",\n )\n\n # Resolve provider_id to stored credentials if no direct api_key given\n if not api_key and provider_id:\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n base_url = base_url or db_provider.base_url\n provider_type_str = provider_type_str or db_provider.provider_type\n stored_key = service.get_decrypted_api_key(provider_id)\n if stored_key:\n api_key = stored_key\n\n if not base_url:\n raise HTTPException(status_code=400, detail=\"base_url is required\")\n if not provider_type_str:\n raise HTTPException(status_code=400, detail=\"provider_type is required\")\n\n try:\n provider_type = LLMProviderType(provider_type_str)\n except ValueError:\n raise HTTPException(status_code=400, detail=f\"Invalid provider_type: {provider_type_str}\")\n\n # Release DB connection before the network call to avoid idle-in-transaction\n # blocking other queries while we wait for the LLM API response.\n db.close()\n\n client = LLMClient(\n provider_type=provider_type,\n api_key=api_key or \"sk-placeholder\",\n base_url=base_url,\n default_model=\"\",\n )\n\n try:\n models = await client.fetch_models()\n return {\"models\": models}\n except Exception as e:\n logger.error(\n f\"[llm_routes.fetch_models] Failed to fetch models: {e}\",\n extra={\"src\": \"llm_routes.fetch_models\"},\n )\n raise HTTPException(status_code=502, detail=str(e))\n\n\n# #endregion fetch_models\n\n\n# #region get_llm_status [C:4] [TYPE Function]\n# @BRIEF Returns whether LLM runtime is configured for dashboard validation.\n# @PRE: User is authenticated.\n# @POST: configured=true only when an active provider with valid decrypted key exists.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION CALLS -> [_is_valid_runtime_api_key]\n@router.get(\"/status\")\nasync def get_llm_status(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n active_provider = next((p for p in providers if p.is_active), None)\n\n if not providers:\n return {\n \"configured\": False,\n \"reason\": \"no_providers_configured\",\n \"provider_count\": 0,\n \"active_provider_count\": 0,\n }\n\n if not active_provider:\n return {\n \"configured\": False,\n \"reason\": \"no_active_provider\",\n \"provider_count\": len(providers),\n \"active_provider_count\": 0,\n \"providers\": [\n {\n \"id\": provider.id,\n \"name\": provider.name,\n \"provider_type\": provider.provider_type,\n \"is_active\": bool(provider.is_active),\n }\n for provider in providers\n ],\n }\n\n api_key = service.get_decrypted_api_key(active_provider.id)\n if not _is_valid_runtime_api_key(api_key):\n return {\n \"configured\": False,\n \"reason\": \"invalid_api_key\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n return {\n \"configured\": True,\n \"reason\": \"ok\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n\n# #endregion get_llm_status\n\n\n# #region create_provider [C:4] [TYPE Function]\n# @BRIEF Create a new LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the created LLMProviderConfig.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\n \"/providers\", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED\n)\nasync def create_provider(\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Create a new LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.create_provider(config)\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=mask_api_key(config.api_key),\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# #endregion create_provider\n\n\n# #region update_provider [C:4] [TYPE Function]\n# @BRIEF Update an existing LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the updated LLMProviderConfig.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.put(\"/providers/{provider_id}\", response_model=LLMProviderConfig)\nasync def update_provider(\n provider_id: str,\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Update an existing LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.update_provider(provider_id, config)\n if not provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=mask_api_key(service.get_decrypted_api_key(provider.id)) if provider.api_key else \"\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# #endregion update_provider\n\n\n# #region delete_provider [C:4] [TYPE Function]\n# @BRIEF Delete an LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns success status.\n# @RELATION CALLS -> [LLMProviderService]\n@router.delete(\"/providers/{provider_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_provider(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Delete an LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n if not service.delete_provider(provider_id):\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n return\n\n\n# #endregion delete_provider\n\n\n# #region test_connection [C:4] [TYPE Function]\n# @BRIEF Test connection to an LLM provider.\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMClient]\n@router.post(\"/providers/{provider_id}/test\")\nasync def test_connection(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n logger.reason(\n f\"Testing connection for provider_id: {provider_id}\",\n extra={\"src\": \"llm_routes.test_connection\"},\n )\n \"\"\"\n Test connection to an LLM provider.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n api_key = service.get_decrypted_api_key(provider_id)\n\n # Check if API key was successfully decrypted\n if not api_key:\n logger.error(\n f\"[llm_routes][test_connection] Failed to decrypt API key for provider {provider_id}\"\n )\n raise HTTPException(\n status_code=500,\n detail=\"Failed to decrypt API key. The provider may have been encrypted with a different encryption key. Please update the provider with a new API key.\",\n )\n\n client = LLMClient(\n provider_type=LLMProviderType(db_provider.provider_type),\n api_key=api_key,\n base_url=db_provider.base_url,\n default_model=db_provider.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# #endregion test_connection\n\n\n# #region test_provider_config [C:4] [TYPE Function]\n# @BRIEF Test connection with a provided configuration (not yet saved).\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION DEPENDS_ON -> [LLMClient]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\"/providers/test\")\nasync def test_provider_config(\n config: LLMProviderConfig, current_user: User = Depends(get_current_active_user)\n):\n \"\"\"\n Test connection with a provided configuration.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n logger.reason(\n f\"Testing config for {config.name}\",\n extra={\"src\": \"llm_routes.test_provider_config\"},\n )\n\n # Check if API key is provided (reject empty, placeholder, or masked keys)\n if is_masked_or_placeholder(config.api_key):\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# #endregion test_provider_config\n\n# #endregion LlmRoutes\n" + "body": "# #region LlmRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, llm, api, provider]\n# @BRIEF API routes for LLM provider configuration and management.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, Field\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import logger\nfrom ...dependencies import get_current_user as get_current_active_user\nfrom ...plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType\nfrom ...schemas.auth import User\nfrom ...services.llm_provider import LLMProviderService, is_masked_or_placeholder, mask_api_key\n\n\n# #region FetchModelsRequest [C:1] [TYPE Class]\n# @BRIEF Pydantic request model for the fetch-models endpoint.\nclass FetchModelsRequest(BaseModel):\n base_url: str | None = Field(None, description=\"LLM provider base URL\")\n provider_type: str | None = Field(None, description=\"Provider type (openai, anthropic, etc)\")\n api_key: str | None = Field(None, description=\"Direct API key (takes precedence over provider_id)\")\n provider_id: str | None = Field(None, description=\"Saved provider ID to look up stored API key\")\n\n\n# #endregion FetchModelsRequest\n\n# #region router [C:1] [TYPE Global]\n# @BRIEF APIRouter instance for LLM routes.\n\nrouter = APIRouter(tags=[\"LLM\"])\n# #endregion router\n# #endregion router\n\n\n# #region _is_valid_runtime_api_key [C:4] [TYPE Function]\n# @BRIEF Validate decrypted runtime API key presence/shape.\n# @PRE: value can be None.\n# @POST: Returns True only for non-placeholder key.\n# @RELATION BINDS_TO -> [LlmRoutes]\ndef _is_valid_runtime_api_key(value: str | None) -> bool:\n key = (value or \"\").strip()\n if not key:\n return False\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return False\n return len(key) >= 16\n\n\n# #endregion _is_valid_runtime_api_key\n\n\n# #region get_providers [C:4] [TYPE Function]\n# @BRIEF Retrieve all LLM provider configurations.\n# @PRE: User is authenticated.\n# @POST: Returns list of LLMProviderConfig.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.get(\"/providers\", response_model=list[LLMProviderConfig])\nasync def get_providers(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n \"\"\"\n Get all LLM provider configurations.\n \"\"\"\n logger.reason(\n f\"Fetching providers for user: {current_user.username}\",\n extra={\"src\": \"llm_routes.get_providers\"},\n )\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n return [\n LLMProviderConfig(\n id=p.id,\n provider_type=LLMProviderType(p.provider_type),\n name=p.name,\n base_url=p.base_url,\n api_key=mask_api_key(service.get_decrypted_api_key(p.id)) if p.api_key else \"\",\n default_model=p.default_model,\n is_active=p.is_active,\n )\n for p in providers\n ]\n\n\n# #endregion get_providers\n\n\n# #region fetch_models [C:4] [TYPE Function]\n# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.\n# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.\n# @POST: Returns a list of available model IDs.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION CALLS -> [LLMClient]\n@router.post(\"/providers/fetch-models\")\nasync def fetch_models(\n payload: FetchModelsRequest,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n from ...plugins.llm_analysis.models import LLMProviderType\n from ...plugins.llm_analysis.service import LLMClient\n\n base_url = (payload.base_url or \"\").strip()\n provider_type_str = (payload.provider_type or \"\").strip()\n api_key = payload.api_key or \"\"\n provider_id = payload.provider_id or \"\"\n\n # Treat masked/placeholder API keys as if no api_key was provided;\n # they are display-only and cannot be used for real API calls.\n if is_masked_or_placeholder(api_key):\n api_key = \"\"\n\n # Early validation: at least one of api_key or provider_id is required\n if not api_key and not provider_id:\n raise HTTPException(\n status_code=400,\n detail=\"api_key or provider_id is required\",\n )\n\n # Resolve provider_id to stored credentials if no direct api_key given\n if not api_key and provider_id:\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n base_url = base_url or db_provider.base_url\n provider_type_str = provider_type_str or db_provider.provider_type\n stored_key = service.get_decrypted_api_key(provider_id)\n if stored_key:\n api_key = stored_key\n\n if not base_url:\n raise HTTPException(status_code=400, detail=\"base_url is required\")\n if not provider_type_str:\n raise HTTPException(status_code=400, detail=\"provider_type is required\")\n\n try:\n provider_type = LLMProviderType(provider_type_str)\n except ValueError:\n raise HTTPException(status_code=400, detail=f\"Invalid provider_type: {provider_type_str}\")\n\n # Release DB connection before the network call to avoid idle-in-transaction\n # blocking other queries while we wait for the LLM API response.\n db.close()\n\n client = LLMClient(\n provider_type=provider_type,\n api_key=api_key or \"sk-placeholder\",\n base_url=base_url,\n default_model=\"\",\n )\n\n try:\n models = await client.fetch_models()\n return {\"models\": models}\n except Exception as e:\n logger.error(\n f\"[llm_routes.fetch_models] Failed to fetch models: {e}\",\n extra={\"src\": \"llm_routes.fetch_models\"},\n )\n raise HTTPException(status_code=502, detail=str(e))\n\n\n# #endregion fetch_models\n\n\n# #region get_llm_status [C:4] [TYPE Function]\n# @BRIEF Returns whether LLM runtime is configured for dashboard validation.\n# @PRE: User is authenticated.\n# @POST: configured=true only when an active provider with valid decrypted key exists.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION CALLS -> [_is_valid_runtime_api_key]\n@router.get(\"/status\")\nasync def get_llm_status(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n active_provider = next((p for p in providers if p.is_active), None)\n\n if not providers:\n return {\n \"configured\": False,\n \"reason\": \"no_providers_configured\",\n \"provider_count\": 0,\n \"active_provider_count\": 0,\n }\n\n if not active_provider:\n return {\n \"configured\": False,\n \"reason\": \"no_active_provider\",\n \"provider_count\": len(providers),\n \"active_provider_count\": 0,\n \"providers\": [\n {\n \"id\": provider.id,\n \"name\": provider.name,\n \"provider_type\": provider.provider_type,\n \"is_active\": bool(provider.is_active),\n }\n for provider in providers\n ],\n }\n\n api_key = service.get_decrypted_api_key(active_provider.id)\n if not _is_valid_runtime_api_key(api_key):\n return {\n \"configured\": False,\n \"reason\": \"invalid_api_key\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n return {\n \"configured\": True,\n \"reason\": \"ok\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n\n# #endregion get_llm_status\n\n\n# #region create_provider [C:4] [TYPE Function]\n# @BRIEF Create a new LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the created LLMProviderConfig.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\n \"/providers\", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED\n)\nasync def create_provider(\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Create a new LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.create_provider(config)\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=mask_api_key(config.api_key),\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# #endregion create_provider\n\n\n# #region update_provider [C:4] [TYPE Function]\n# @BRIEF Update an existing LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the updated LLMProviderConfig.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.put(\"/providers/{provider_id}\", response_model=LLMProviderConfig)\nasync def update_provider(\n provider_id: str,\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Update an existing LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.update_provider(provider_id, config)\n if not provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=mask_api_key(service.get_decrypted_api_key(provider.id)) if provider.api_key else \"\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# #endregion update_provider\n\n\n# #region delete_provider [C:4] [TYPE Function]\n# @BRIEF Delete an LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns success status.\n# @RELATION CALLS -> [LLMProviderService]\n@router.delete(\"/providers/{provider_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_provider(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Delete an LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n if not service.delete_provider(provider_id):\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n return\n\n\n# #endregion delete_provider\n\n\n# #region test_connection [C:4] [TYPE Function]\n# @BRIEF Test connection to an LLM provider.\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMClient]\n@router.post(\"/providers/{provider_id}/test\")\nasync def test_connection(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n logger.reason(\n f\"Testing connection for provider_id: {provider_id}\",\n extra={\"src\": \"llm_routes.test_connection\"},\n )\n \"\"\"\n Test connection to an LLM provider.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n api_key = service.get_decrypted_api_key(provider_id)\n\n # Check if API key was successfully decrypted\n if not api_key:\n logger.error(\n f\"[llm_routes][test_connection] Failed to decrypt API key for provider {provider_id}\"\n )\n raise HTTPException(\n status_code=500,\n detail=\"Failed to decrypt API key. The provider may have been encrypted with a different encryption key. Please update the provider with a new API key.\",\n )\n\n client = LLMClient(\n provider_type=LLMProviderType(db_provider.provider_type),\n api_key=api_key,\n base_url=db_provider.base_url,\n default_model=db_provider.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# #endregion test_connection\n\n\n# #region test_provider_config [C:4] [TYPE Function]\n# @BRIEF Test connection with a provided configuration (not yet saved).\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION DEPENDS_ON -> [LLMClient]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\"/providers/test\")\nasync def test_provider_config(\n config: LLMProviderConfig, current_user: User = Depends(get_current_active_user)\n):\n \"\"\"\n Test connection with a provided configuration.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n logger.reason(\n f\"Testing config for {config.name}\",\n extra={\"src\": \"llm_routes.test_provider_config\"},\n )\n\n # Check if API key is provided (reject empty, placeholder, or masked keys)\n if is_masked_or_placeholder(config.api_key):\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# #endregion test_provider_config\n\n# #endregion LlmRoutes\n" }, { "contract_id": "FetchModelsRequest", @@ -18509,14 +18442,41 @@ "contract_type": "Global", "file_path": "backend/src/api/routes/llm.py", "start_line": 33, - "end_line": 36, + "end_line": 37, "tier": "TIER_1", "complexity": 1, "metadata": { + "COMPLEXITY": 1, "PURPOSE": "APIRouter instance for LLM routes." }, "relations": [], "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Global'", + "detail": { + "actual_type": "Global", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Global' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Global" + } + }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -18547,14 +18507,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region router [TYPE Global]\n# @BRIEF APIRouter instance for LLM routes.\nrouter = APIRouter(tags=[\"LLM\"])\n# #endregion router\n" + "body": "# #region router [C:1] [TYPE Global]\n# @BRIEF APIRouter instance for LLM routes.\n\nrouter = APIRouter(tags=[\"LLM\"])\n# #endregion router\n" }, { "contract_id": "_is_valid_runtime_api_key", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 39, - "end_line": 53, + "start_line": 41, + "end_line": 55, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18590,8 +18550,8 @@ "contract_id": "get_providers", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 56, - "end_line": 89, + "start_line": 58, + "end_line": 91, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18633,8 +18593,8 @@ "contract_id": "fetch_models", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 92, - "end_line": 168, + "start_line": 94, + "end_line": 170, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18676,8 +18636,8 @@ "contract_id": "get_llm_status", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 171, - "end_line": 239, + "start_line": 173, + "end_line": 241, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18719,8 +18679,8 @@ "contract_id": "create_provider", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 242, - "end_line": 272, + "start_line": 244, + "end_line": 274, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18762,8 +18722,8 @@ "contract_id": "update_provider", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 275, - "end_line": 307, + "start_line": 277, + "end_line": 309, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18805,8 +18765,8 @@ "contract_id": "delete_provider", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 310, - "end_line": 330, + "start_line": 312, + "end_line": 332, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18842,8 +18802,8 @@ "contract_id": "test_connection", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 333, - "end_line": 385, + "start_line": 335, + "end_line": 387, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -18885,8 +18845,8 @@ "contract_id": "test_provider_config", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 388, - "end_line": 428, + "start_line": 390, + "end_line": 430, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -19625,7 +19585,7 @@ "complexity": 3, "metadata": { "COMPLEXITY": 3, - "LAYER": "UI (API)", + "LAYER": "API", "PURPOSE": "Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins." }, "relations": [ @@ -19648,28 +19608,10 @@ "target_ref": "[API_Routes]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region PluginsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, plugin, api]\n# @BRIEF Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [PluginConfig]\n# @RELATION DEPENDS_ON -> [get_plugin_loader]\n# @RELATION BINDS_TO -> [API_Routes]\n\nfrom fastapi import APIRouter, Depends\n\nfrom ...core.logger import belief_scope\nfrom ...core.plugin_base import PluginConfig\nfrom ...dependencies import get_plugin_loader, has_permission\n\nrouter = APIRouter()\n\n\n# #region list_plugins [TYPE Function]\n# @BRIEF Retrieve a list of all available plugins.\n# @PRE: plugin_loader is injected via Depends.\n# @POST: Returns a list of PluginConfig objects.\n# @RELATION CALLS -> [get_plugin_loader]\n# @RELATION DEPENDS_ON -> [PluginConfig]\n@router.get(\"\", response_model=list[PluginConfig])\nasync def list_plugins(\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"plugins\", \"READ\")),\n):\n with belief_scope(\"list_plugins\"):\n \"\"\"\n Retrieve a list of all available plugins.\n \"\"\"\n return plugin_loader.get_all_plugin_configs()\n\n\n# #endregion list_plugins\n# #endregion PluginsRouter\n" + "body": "# #region PluginsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, plugin, api]\n# @BRIEF Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [PluginConfig]\n# @RELATION DEPENDS_ON -> [get_plugin_loader]\n# @RELATION BINDS_TO -> [API_Routes]\n\nfrom fastapi import APIRouter, Depends\n\nfrom ...core.logger import belief_scope\nfrom ...core.plugin_base import PluginConfig\nfrom ...dependencies import get_plugin_loader, has_permission\n\nrouter = APIRouter()\n\n\n# #region list_plugins [TYPE Function]\n# @BRIEF Retrieve a list of all available plugins.\n# @PRE: plugin_loader is injected via Depends.\n# @POST: Returns a list of PluginConfig objects.\n# @RELATION CALLS -> [get_plugin_loader]\n# @RELATION DEPENDS_ON -> [PluginConfig]\n@router.get(\"\", response_model=list[PluginConfig])\nasync def list_plugins(\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"plugins\", \"READ\")),\n):\n with belief_scope(\"list_plugins\"):\n \"\"\"\n Retrieve a list of all available plugins.\n \"\"\"\n return plugin_loader.get_all_plugin_configs()\n\n\n# #endregion list_plugins\n# #endregion PluginsRouter\n" }, { "contract_id": "list_plugins", @@ -19736,14 +19678,18 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/profile.py", "start_line": 1, - "end_line": 146, - "tier": "TIER_2", - "complexity": 3, + "end_line": 150, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ProfileRequest -> ProfileResponse", "INVARIANT": "Endpoints are self-scoped and never mutate another user preference.", "LAYER": "API", + "POST": "Profile endpoints registered", + "PRE": "Auth middleware configured, database session available", "PURPOSE": "Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.", + "SIDE_EFFECT": "Registers /api/profile/* routes", "UX_FEEDBACK": "Stable status/message/warning payloads support profile page feedback.", "UX_RECOVERY": "Lookup degradation keeps manual username save path available.", "UX_STATE": "LookupLoading -> Returns success/degraded Superset lookup payload." @@ -19769,15 +19715,6 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "tag_not_for_contract_type", "tag": "UX_FEEDBACK", @@ -19792,9 +19729,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C3", + "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, "contract_type": "Module" } }, @@ -19812,9 +19749,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C3", + "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, "contract_type": "Module" } }, @@ -19832,23 +19769,23 @@ { "code": "tag_forbidden_by_complexity", "tag": "UX_STATE", - "message": "@UX_STATE is forbidden for contract type 'Module' at C3", + "message": "@UX_STATE is forbidden for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileApiModule [C:3] [TYPE Module] [SEMANTICS fastapi, profile, api, validate, search, superset]\n#\n# @BRIEF Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [ProfileService]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n#\n# @INVARIANT: Endpoints are self-scoped and never mutate another user preference.\n# @UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user.\n# @UX_STATE: Saving -> Validation errors map to actionable 422 details.\n# @UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload.\n# @UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback.\n# @UX_RECOVERY: Lookup degradation keeps manual username save path available.\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import (\n get_config_manager,\n get_current_user,\n get_plugin_loader,\n)\nfrom ...models.auth import User\nfrom ...schemas.profile import (\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom ...services.profile_service import (\n EnvironmentNotFoundError,\n ProfileAuthorizationError,\n ProfileService,\n ProfileValidationError,\n)\n\nrouter = APIRouter(prefix=\"/api/profile\", tags=[\"profile\"])\n\n\n# #region _get_profile_service [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Build profile service for current request scope.\n# @PRE: db session and config manager are available.\n# @POST: Returns a ready ProfileService instance.\ndef _get_profile_service(db: Session, config_manager, plugin_loader=None) -> ProfileService:\n return ProfileService(\n db=db,\n config_manager=config_manager,\n plugin_loader=plugin_loader,\n )\n# #endregion _get_profile_service\n\n\n# #region get_preferences [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Get authenticated user's dashboard filter preference.\n# @PRE: Valid JWT and authenticated user context.\n# @POST: Returns preference payload for current user only.\n@router.get(\"/preferences\", response_model=ProfilePreferenceResponse)\nasync def get_preferences(\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n config_manager=Depends(get_config_manager),\n plugin_loader=Depends(get_plugin_loader),\n):\n with belief_scope(\"profile.get_preferences\", f\"user_id={current_user.id}\"):\n logger.reason(\"Resolving current user preference\")\n service = _get_profile_service(db, config_manager, plugin_loader)\n return service.get_my_preference(current_user)\n# #endregion get_preferences\n\n\n# #region update_preferences [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Update authenticated user's dashboard filter preference.\n# @PRE: Valid JWT and valid request payload.\n# @POST: Persists normalized preference for current user or raises validation/authorization errors.\n@router.patch(\"/preferences\", response_model=ProfilePreferenceResponse)\nasync def update_preferences(\n payload: ProfilePreferenceUpdateRequest,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n config_manager=Depends(get_config_manager),\n plugin_loader=Depends(get_plugin_loader),\n):\n with belief_scope(\"profile.update_preferences\", f\"user_id={current_user.id}\"):\n service = _get_profile_service(db, config_manager, plugin_loader)\n try:\n logger.reason(\"Attempting preference save\")\n return service.update_my_preference(current_user=current_user, payload=payload)\n except ProfileValidationError as exc:\n logger.reflect(\"Preference validation failed\")\n raise HTTPException(status_code=422, detail=exc.errors) from exc\n except ProfileAuthorizationError as exc:\n logger.explore(\"Cross-user mutation guard blocked request\")\n raise HTTPException(status_code=403, detail=str(exc)) from exc\n# #endregion update_preferences\n\n\n# #region lookup_superset_accounts [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Lookup Superset account candidates in selected environment.\n# @PRE: Valid JWT, authenticated context, and environment_id query parameter.\n# @POST: Returns success or degraded lookup payload with stable shape.\n@router.get(\"/superset-accounts\", response_model=SupersetAccountLookupResponse)\nasync def lookup_superset_accounts(\n environment_id: str = Query(...),\n search: str | None = Query(default=None),\n page_index: int = Query(default=0, ge=0),\n page_size: int = Query(default=20, ge=1, le=100),\n sort_column: str = Query(default=\"username\"),\n sort_order: str = Query(default=\"desc\"),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n config_manager=Depends(get_config_manager),\n plugin_loader=Depends(get_plugin_loader),\n):\n with belief_scope(\n \"profile.lookup_superset_accounts\",\n f\"user_id={current_user.id}, environment_id={environment_id}\",\n ):\n service = _get_profile_service(db, config_manager, plugin_loader)\n lookup_request = SupersetAccountLookupRequest(\n environment_id=environment_id,\n search=search,\n page_index=page_index,\n page_size=page_size,\n sort_column=sort_column,\n sort_order=sort_order,\n )\n try:\n logger.reason(\"Executing Superset account lookup\")\n return service.lookup_superset_accounts(\n current_user=current_user,\n request=lookup_request,\n )\n except EnvironmentNotFoundError as exc:\n logger.explore(\"Lookup request references unknown environment\")\n raise HTTPException(status_code=404, detail=str(exc)) from exc\n# #endregion lookup_superset_accounts\n\n# #endregion ProfileApiModule\n" + "body": "# #region ProfileApiModule [C:5] [TYPE Module] [SEMANTICS fastapi, profile, api, validate, search, superset]\n#\n# @BRIEF Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [ProfileService]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n#\n# @INVARIANT: Endpoints are self-scoped and never mutate another user preference.\n# @PRE: Auth middleware configured, database session available\n# @POST: Profile endpoints registered\n# @UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user.\n# @UX_STATE: Saving -> Validation errors map to actionable 422 details.\n# @UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload.\n# @UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback.\n# @UX_RECOVERY: Lookup degradation keeps manual username save path available.\n# @SIDE_EFFECT: Registers /api/profile/* routes\n# @DATA_CONTRACT: ProfileRequest -> ProfileResponse\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import (\n get_config_manager,\n get_current_user,\n get_plugin_loader,\n)\nfrom ...models.auth import User\nfrom ...schemas.profile import (\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom ...services.profile_service import (\n EnvironmentNotFoundError,\n ProfileAuthorizationError,\n ProfileService,\n ProfileValidationError,\n)\n\nrouter = APIRouter(prefix=\"/api/profile\", tags=[\"profile\"])\n\n\n# #region _get_profile_service [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Build profile service for current request scope.\n# @PRE: db session and config manager are available.\n# @POST: Returns a ready ProfileService instance.\ndef _get_profile_service(db: Session, config_manager, plugin_loader=None) -> ProfileService:\n return ProfileService(\n db=db,\n config_manager=config_manager,\n plugin_loader=plugin_loader,\n )\n# #endregion _get_profile_service\n\n\n# #region get_preferences [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Get authenticated user's dashboard filter preference.\n# @PRE: Valid JWT and authenticated user context.\n# @POST: Returns preference payload for current user only.\n@router.get(\"/preferences\", response_model=ProfilePreferenceResponse)\nasync def get_preferences(\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n config_manager=Depends(get_config_manager),\n plugin_loader=Depends(get_plugin_loader),\n):\n with belief_scope(\"profile.get_preferences\", f\"user_id={current_user.id}\"):\n logger.reason(\"Resolving current user preference\")\n service = _get_profile_service(db, config_manager, plugin_loader)\n return service.get_my_preference(current_user)\n# #endregion get_preferences\n\n\n# #region update_preferences [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Update authenticated user's dashboard filter preference.\n# @PRE: Valid JWT and valid request payload.\n# @POST: Persists normalized preference for current user or raises validation/authorization errors.\n@router.patch(\"/preferences\", response_model=ProfilePreferenceResponse)\nasync def update_preferences(\n payload: ProfilePreferenceUpdateRequest,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n config_manager=Depends(get_config_manager),\n plugin_loader=Depends(get_plugin_loader),\n):\n with belief_scope(\"profile.update_preferences\", f\"user_id={current_user.id}\"):\n service = _get_profile_service(db, config_manager, plugin_loader)\n try:\n logger.reason(\"Attempting preference save\")\n return service.update_my_preference(current_user=current_user, payload=payload)\n except ProfileValidationError as exc:\n logger.reflect(\"Preference validation failed\")\n raise HTTPException(status_code=422, detail=exc.errors) from exc\n except ProfileAuthorizationError as exc:\n logger.explore(\"Cross-user mutation guard blocked request\")\n raise HTTPException(status_code=403, detail=str(exc)) from exc\n# #endregion update_preferences\n\n\n# #region lookup_superset_accounts [TYPE Function]\n# @RELATION CALLS -> ProfileService\n# @BRIEF Lookup Superset account candidates in selected environment.\n# @PRE: Valid JWT, authenticated context, and environment_id query parameter.\n# @POST: Returns success or degraded lookup payload with stable shape.\n@router.get(\"/superset-accounts\", response_model=SupersetAccountLookupResponse)\nasync def lookup_superset_accounts(\n environment_id: str = Query(...),\n search: str | None = Query(default=None),\n page_index: int = Query(default=0, ge=0),\n page_size: int = Query(default=20, ge=1, le=100),\n sort_column: str = Query(default=\"username\"),\n sort_order: str = Query(default=\"desc\"),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n config_manager=Depends(get_config_manager),\n plugin_loader=Depends(get_plugin_loader),\n):\n with belief_scope(\n \"profile.lookup_superset_accounts\",\n f\"user_id={current_user.id}, environment_id={environment_id}\",\n ):\n service = _get_profile_service(db, config_manager, plugin_loader)\n lookup_request = SupersetAccountLookupRequest(\n environment_id=environment_id,\n search=search,\n page_index=page_index,\n page_size=page_size,\n sort_column=sort_column,\n sort_order=sort_order,\n )\n try:\n logger.reason(\"Executing Superset account lookup\")\n return service.lookup_superset_accounts(\n current_user=current_user,\n request=lookup_request,\n )\n except EnvironmentNotFoundError as exc:\n logger.explore(\"Lookup request references unknown environment\")\n raise HTTPException(status_code=404, detail=str(exc)) from exc\n# #endregion lookup_superset_accounts\n\n# #endregion ProfileApiModule\n" }, { "contract_id": "_get_profile_service", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 44, - "end_line": 55, + "start_line": 48, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19901,8 +19838,8 @@ "contract_id": "get_preferences", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 58, - "end_line": 74, + "start_line": 62, + "end_line": 78, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19955,8 +19892,8 @@ "contract_id": "update_preferences", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 77, - "end_line": 101, + "start_line": 81, + "end_line": 105, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20009,8 +19946,8 @@ "contract_id": "lookup_superset_accounts", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 104, - "end_line": 144, + "start_line": 108, + "end_line": 148, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20071,7 +20008,7 @@ "COMPLEXITY": 5, "DATA_CONTRACT": "[ReportQuery] -> [ReportCollection | ReportDetailView]", "INVARIANT": "Endpoints are read-only and do not trigger long-running tasks.", - "LAYER": "UI (API)", + "LAYER": "API", "POST": "Router is configured and endpoints are ready for registration.", "PRE": "Reports service and dependencies are initialized.", "PURPOSE": "FastAPI router for unified task report list and detail retrieval endpoints.", @@ -20103,28 +20040,10 @@ "target_ref": "[has_permission:Function]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, report, api, transform, search, task]\n# @BRIEF FastAPI router for unified task report list and detail retrieval endpoints.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [ReportsService:Class]\n# @RELATION DEPENDS_ON -> [get_task_manager:Function]\n# @RELATION DEPENDS_ON -> [get_clean_release_repository:Function]\n# @RELATION DEPENDS_ON -> [has_permission:Function]\n# @INVARIANT: Endpoints are read-only and do not trigger long-running tasks.\n# @PRE: Reports service and dependencies are initialized.\n# @POST: Router is configured and endpoints are ready for registration.\n# @SIDE_EFFECT: None\n# @DATA_CONTRACT: [ReportQuery] -> [ReportCollection | ReportDetailView]\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\n\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager import TaskManager\nfrom ...dependencies import (\n get_clean_release_repository,\n get_task_manager,\n has_permission,\n)\nfrom ...models.report import (\n ReportCollection,\n ReportDetailView,\n ReportQuery,\n ReportStatus,\n TaskType,\n)\nfrom ...services.clean_release.repository import CleanReleaseRepository\nfrom ...services.reports.report_service import ReportsService\n\nrouter = APIRouter(prefix=\"/api/reports\", tags=[\"Reports\"])\n\n\n# #region _parse_csv_enum_list [C:1] [TYPE Function]\n# @BRIEF Parse comma-separated query value into enum list.\n# @PRE: raw may be None/empty or comma-separated values.\n# @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload.\n# @RELATION BINDS_TO -> [ReportsRouter]\ndef _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:\n with belief_scope(\"_parse_csv_enum_list\"):\n if raw is None or not raw.strip():\n return []\n values = [item.strip() for item in raw.split(\",\") if item.strip()]\n parsed = []\n invalid = []\n for value in values:\n try:\n parsed.append(enum_cls(value))\n except ValueError:\n invalid.append(value)\n if invalid:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\n \"message\": f\"Invalid values for '{field_name}'\",\n \"field\": field_name,\n \"invalid_values\": invalid,\n \"allowed_values\": [item.value for item in enum_cls],\n },\n )\n return parsed\n\n\n# #endregion _parse_csv_enum_list\n\n\n# #region list_reports [C:2] [TYPE Function]\n# @BRIEF Return paginated unified reports list.\n# @PRE: authenticated/authorized request and validated query params.\n# @POST: returns {items,total,page,page_size,has_next,applied_filters}.\n# @POST: deterministic error payload for invalid filters.\n# @RELATION CALLS -> [_parse_csv_enum_list:Function]\n# @RELATION DEPENDS_ON -> [ReportQuery:Class]\n# @RELATION DEPENDS_ON -> [ReportsService:Class]\n#\n# @TEST_CONTRACT: ListReportsApi ->\n# {\n# required_fields: {page: int, page_size: int, sort_by: str, sort_order: str},\n# optional_fields: {task_types: str, statuses: str, search: str},\n# invariants: [\n# \"Returns ReportCollection on success\",\n# \"Raises HTTPException 400 for invalid query parameters\"\n# ]\n# }\n# @TEST_FIXTURE: valid_list_request -> {\"page\": 1, \"page_size\": 20}\n# @TEST_EDGE: invalid_task_type_filter -> raises HTTPException(400)\n# @TEST_EDGE: malformed_query -> raises HTTPException(400)\n# @TEST_INVARIANT: consistent_list_payload -> verifies: [valid_list_request]\n@router.get(\"\", response_model=ReportCollection)\nasync def list_reports(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n task_types: str | None = Query(None, description=\"Comma-separated task types\"),\n statuses: str | None = Query(None, description=\"Comma-separated statuses\"),\n time_from: datetime | None = Query(None),\n time_to: datetime | None = Query(None),\n search: str | None = Query(None, max_length=200),\n sort_by: str = Query(\"updated_at\"),\n sort_order: str = Query(\"desc\"),\n task_manager: TaskManager = Depends(get_task_manager),\n clean_release_repository: CleanReleaseRepository = Depends(\n get_clean_release_repository\n ),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_reports\"):\n try:\n parsed_task_types = _parse_csv_enum_list(task_types, TaskType, \"task_types\")\n parsed_statuses = _parse_csv_enum_list(statuses, ReportStatus, \"statuses\")\n query = ReportQuery(\n page=page,\n page_size=page_size,\n task_types=parsed_task_types,\n statuses=parsed_statuses,\n time_from=time_from,\n time_to=time_to,\n search=search,\n sort_by=sort_by,\n sort_order=sort_order,\n )\n except HTTPException:\n raise\n except Exception as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\n \"message\": \"Invalid query parameters\",\n \"code\": \"INVALID_REPORT_QUERY\",\n \"reason\": str(exc),\n },\n )\n\n service = ReportsService(\n task_manager, clean_release_repository=clean_release_repository\n )\n return service.list_reports(query)\n\n\n# #endregion list_reports\n\n\n# #region get_report_detail [C:2] [TYPE Function]\n# @BRIEF Return one normalized report detail with diagnostics and next actions.\n# @PRE: authenticated/authorized request and existing report_id.\n# @POST: returns normalized detail envelope or 404 when report is not found.\n# @RELATION CALLS -> [ReportsService:Class]\n@router.get(\"/{report_id}\", response_model=ReportDetailView)\nasync def get_report_detail(\n report_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n clean_release_repository: CleanReleaseRepository = Depends(\n get_clean_release_repository\n ),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_report_detail\", f\"report_id={report_id}\"):\n service = ReportsService(\n task_manager, clean_release_repository=clean_release_repository\n )\n detail = service.get_report_detail(report_id)\n if not detail:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail={\"message\": \"Report not found\", \"code\": \"REPORT_NOT_FOUND\"},\n )\n return detail\n\n\n# #endregion get_report_detail\n\n# #endregion ReportsRouter\n" + "body": "# #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, report, api, transform, search, task]\n# @BRIEF FastAPI router for unified task report list and detail retrieval endpoints.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [ReportsService:Class]\n# @RELATION DEPENDS_ON -> [get_task_manager:Function]\n# @RELATION DEPENDS_ON -> [get_clean_release_repository:Function]\n# @RELATION DEPENDS_ON -> [has_permission:Function]\n# @INVARIANT: Endpoints are read-only and do not trigger long-running tasks.\n# @PRE: Reports service and dependencies are initialized.\n# @POST: Router is configured and endpoints are ready for registration.\n# @SIDE_EFFECT: None\n# @DATA_CONTRACT: [ReportQuery] -> [ReportCollection | ReportDetailView]\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\n\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager import TaskManager\nfrom ...dependencies import (\n get_clean_release_repository,\n get_task_manager,\n has_permission,\n)\nfrom ...models.report import (\n ReportCollection,\n ReportDetailView,\n ReportQuery,\n ReportStatus,\n TaskType,\n)\nfrom ...services.clean_release.repository import CleanReleaseRepository\nfrom ...services.reports.report_service import ReportsService\n\nrouter = APIRouter(prefix=\"/api/reports\", tags=[\"Reports\"])\n\n\n# #region _parse_csv_enum_list [C:1] [TYPE Function]\n# @BRIEF Parse comma-separated query value into enum list.\n# @PRE: raw may be None/empty or comma-separated values.\n# @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload.\n# @RELATION BINDS_TO -> [ReportsRouter]\ndef _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:\n with belief_scope(\"_parse_csv_enum_list\"):\n if raw is None or not raw.strip():\n return []\n values = [item.strip() for item in raw.split(\",\") if item.strip()]\n parsed = []\n invalid = []\n for value in values:\n try:\n parsed.append(enum_cls(value))\n except ValueError:\n invalid.append(value)\n if invalid:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\n \"message\": f\"Invalid values for '{field_name}'\",\n \"field\": field_name,\n \"invalid_values\": invalid,\n \"allowed_values\": [item.value for item in enum_cls],\n },\n )\n return parsed\n\n\n# #endregion _parse_csv_enum_list\n\n\n# #region list_reports [C:2] [TYPE Function]\n# @BRIEF Return paginated unified reports list.\n# @PRE: authenticated/authorized request and validated query params.\n# @POST: returns {items,total,page,page_size,has_next,applied_filters}.\n# @POST: deterministic error payload for invalid filters.\n# @RELATION CALLS -> [_parse_csv_enum_list:Function]\n# @RELATION DEPENDS_ON -> [ReportQuery:Class]\n# @RELATION DEPENDS_ON -> [ReportsService:Class]\n#\n# @TEST_CONTRACT: ListReportsApi ->\n# {\n# required_fields: {page: int, page_size: int, sort_by: str, sort_order: str},\n# optional_fields: {task_types: str, statuses: str, search: str},\n# invariants: [\n# \"Returns ReportCollection on success\",\n# \"Raises HTTPException 400 for invalid query parameters\"\n# ]\n# }\n# @TEST_FIXTURE: valid_list_request -> {\"page\": 1, \"page_size\": 20}\n# @TEST_EDGE: invalid_task_type_filter -> raises HTTPException(400)\n# @TEST_EDGE: malformed_query -> raises HTTPException(400)\n# @TEST_INVARIANT: consistent_list_payload -> verifies: [valid_list_request]\n@router.get(\"\", response_model=ReportCollection)\nasync def list_reports(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n task_types: str | None = Query(None, description=\"Comma-separated task types\"),\n statuses: str | None = Query(None, description=\"Comma-separated statuses\"),\n time_from: datetime | None = Query(None),\n time_to: datetime | None = Query(None),\n search: str | None = Query(None, max_length=200),\n sort_by: str = Query(\"updated_at\"),\n sort_order: str = Query(\"desc\"),\n task_manager: TaskManager = Depends(get_task_manager),\n clean_release_repository: CleanReleaseRepository = Depends(\n get_clean_release_repository\n ),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_reports\"):\n try:\n parsed_task_types = _parse_csv_enum_list(task_types, TaskType, \"task_types\")\n parsed_statuses = _parse_csv_enum_list(statuses, ReportStatus, \"statuses\")\n query = ReportQuery(\n page=page,\n page_size=page_size,\n task_types=parsed_task_types,\n statuses=parsed_statuses,\n time_from=time_from,\n time_to=time_to,\n search=search,\n sort_by=sort_by,\n sort_order=sort_order,\n )\n except HTTPException:\n raise\n except Exception as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\n \"message\": \"Invalid query parameters\",\n \"code\": \"INVALID_REPORT_QUERY\",\n \"reason\": str(exc),\n },\n )\n\n service = ReportsService(\n task_manager, clean_release_repository=clean_release_repository\n )\n return service.list_reports(query)\n\n\n# #endregion list_reports\n\n\n# #region get_report_detail [C:2] [TYPE Function]\n# @BRIEF Return one normalized report detail with diagnostics and next actions.\n# @PRE: authenticated/authorized request and existing report_id.\n# @POST: returns normalized detail envelope or 404 when report is not found.\n# @RELATION CALLS -> [ReportsService:Class]\n@router.get(\"/{report_id}\", response_model=ReportDetailView)\nasync def get_report_detail(\n report_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n clean_release_repository: CleanReleaseRepository = Depends(\n get_clean_release_repository\n ),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_report_detail\", f\"report_id={report_id}\"):\n service = ReportsService(\n task_manager, clean_release_repository=clean_release_repository\n )\n detail = service.get_report_detail(report_id)\n if not detail:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail={\"message\": \"Report not found\", \"code\": \"REPORT_NOT_FOUND\"},\n )\n return detail\n\n\n# #endregion get_report_detail\n\n# #endregion ReportsRouter\n" }, { "contract_id": "_parse_csv_enum_list", @@ -20309,15 +20228,19 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/settings.py", "start_line": 1, - "end_line": 640, - "tier": "TIER_2", - "complexity": 3, + "end_line": 649, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse", "INVARIANT": "All settings changes must be persisted via ConfigManager.", "LAYER": "API", + "POST": "Settings are read or written via ConfigManager.", + "PRE": "ConfigManager is initialized and accessible.", "PUBLIC_API": "router", - "PURPOSE": "Provides API endpoints for managing application settings and Superset environments." + "PURPOSE": "Provides API endpoints for managing application settings and Superset environments.", + "SIDE_EFFECT": "Persists config changes to disk via ConfigManager." }, "relations": [ { @@ -20340,15 +20263,6 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "unknown_tag", "tag": "PUBLIC_API", @@ -20358,14 +20272,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SettingsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response]\n#\n# @BRIEF Provides API endpoints for managing application settings and Superset environments.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_config_manager]\n# @RELATION DEPENDS_ON -> [has_permission]\n#\n# @INVARIANT: All settings changes must be persisted via ConfigManager.\n# @PUBLIC_API: router\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.config import AppConfigRecord\nfrom ...models.llm import ValidationPolicy\nfrom ...models.storage import StorageConfig\nfrom ...models.translate import TranslationJob, TranslationSchedule\nfrom ...schemas.settings import (\n TranslationScheduleItem,\n ValidationPolicyCreate,\n ValidationPolicyResponse,\n ValidationPolicyUpdate,\n)\nfrom ...services.llm_prompt_templates import normalize_llm_settings\n\n\n# #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response]\n# @BRIEF Response model for logging configuration with current task log level.\nclass LoggingConfigResponse(BaseModel):\n level: str\n task_log_level: str\n enable_belief_state: bool\n\n\n# #endregion LoggingConfigResponse\n\nrouter = APIRouter()\n\n\n# #region _normalize_superset_env_url [C:1] [TYPE Function]\n# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.\n# @PRE: raw_url can be empty.\n# @POST: Returns normalized base URL.\ndef _normalize_superset_env_url(raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[: -len(\"/api/v1\")]\n return normalized.rstrip(\"/\")\n\n\n# #endregion _normalize_superset_env_url\n\n\n# #region _validate_superset_connection_fast [C:2] [TYPE Function]\n# @BRIEF Run lightweight Superset connectivity validation without full pagination scan.\n# @PRE: env contains valid URL and credentials.\n# @POST: Raises on auth/API failures; returns None on success.\ndef _validate_superset_connection_fast(env: Environment) -> None:\n client = SupersetClient(env)\n # 1) Explicit auth check\n client.authenticate()\n # 2) Single lightweight API call to ensure read access\n client.get_dashboards_page(\n query={\n \"page\": 0,\n \"page_size\": 1,\n \"columns\": [\"id\"],\n }\n )\n\n\n# #endregion _validate_superset_connection_fast\n\n\n# #region get_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves all application settings.\n# @PRE: Config manager is available.\n# @POST: Returns masked AppConfig.\n@router.get(\"\", response_model=AppConfig)\nasync def get_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_settings\"):\n logger.reason(\"Fetching all settings\", extra={\"src\": \"get_settings\"})\n config = config_manager.get_config().copy(deep=True)\n config.settings.llm = normalize_llm_settings(config.settings.llm)\n # Mask passwords\n for env in config.environments:\n if env.password:\n env.password = \"********\"\n return config\n\n\n# #endregion get_settings\n\n\n# #region get_features [C:1] [TYPE Function]\n# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering.\n# @RATIONALE: Unauthenticated because sidebar filtering must work for all users, not just admins.\n# @PRE: Config manager is available.\n# @POST: Returns dict with dataset_review and health_monitor booleans.\n@router.get(\"/features\")\nasync def get_features(\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n return config_manager.get_config().settings.features.model_dump()\n\n\n# #endregion get_features\n\n\n# #region update_global_settings [C:2] [TYPE Function]\n# @BRIEF Updates global application settings.\n# @PRE: New settings are provided.\n# @POST: Global settings are updated.\n@router.patch(\"/global\", response_model=GlobalSettings)\nasync def update_global_settings(\n settings: GlobalSettings,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_global_settings\"):\n logger.reason(\"Updating global settings\", extra={\"src\": \"update_global_settings\"})\n\n config_manager.update_global_settings(settings)\n return settings\n\n\n# #endregion update_global_settings\n\n\n# #region get_storage_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves storage-specific settings.\n@router.get(\"/storage\", response_model=StorageConfig)\nasync def get_storage_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_storage_settings\"):\n return config_manager.get_config().settings.storage\n\n\n# #endregion get_storage_settings\n\n\n# #region update_storage_settings [C:2] [TYPE Function]\n# @BRIEF Updates storage-specific settings.\n# @POST: Storage settings are updated and saved.\n@router.put(\"/storage\", response_model=StorageConfig)\nasync def update_storage_settings(\n storage: StorageConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_storage_settings\"):\n is_valid, message = config_manager.validate_path(storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n\n settings = config_manager.get_config().settings\n settings.storage = storage\n config_manager.update_global_settings(settings)\n return config_manager.get_config().settings.storage\n\n\n# #endregion update_storage_settings\n\n\n# #region get_environments [C:2] [TYPE Function]\n# @BRIEF Lists all configured Superset environments.\n# @PRE: Config manager is available.\n# @POST: Returns list of environments.\n@router.get(\"/environments\", response_model=list[Environment])\nasync def get_environments(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n logger.reason(\"Fetching environments\", extra={\"src\": \"get_environments\"})\n environments = config_manager.get_environments()\n return [\n env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n for env in environments\n ]\n\n\n# #endregion get_environments\n\n\n# #region add_environment [C:2] [TYPE Function]\n# @BRIEF Adds a new Superset environment.\n# @PRE: Environment data is valid and reachable.\n# @POST: Environment is added to config.\n@router.post(\"/environments\", response_model=Environment)\nasync def add_environment(\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"add_environment\"):\n logger.reason(f\"Adding environment {env.id}\", extra={\"src\": \"add_environment\"})\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # Validate connection before adding (fast path)\n try:\n _validate_superset_connection_fast(env)\n except Exception as e:\n logger.error(\n f\"[add_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n config_manager.add_environment(env)\n return env\n\n\n# #endregion add_environment\n\n\n# #region update_environment [C:2] [TYPE Function]\n# @BRIEF Updates an existing Superset environment.\n# @PRE: ID and valid environment data are provided.\n# @POST: Environment is updated in config.\n@router.put(\"/environments/{id}\", response_model=Environment)\nasync def update_environment(\n id: str,\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n with belief_scope(\"update_environment\"):\n logger.reason(f\"Updating environment {id}\", extra={\"src\": \"update_environment\"})\n\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # If password is masked, we need the real one for validation\n env_to_validate = env.copy(deep=True)\n if env_to_validate.password == \"********\":\n old_env = next(\n (e for e in config_manager.get_environments() if e.id == id), None\n )\n if old_env:\n env_to_validate.password = old_env.password\n\n # Validate connection before updating (fast path)\n try:\n _validate_superset_connection_fast(env_to_validate)\n except Exception as e:\n logger.error(\n f\"[update_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n if config_manager.update_environment(id, env):\n return env\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n\n# #endregion update_environment\n\n\n# #region delete_environment [C:2] [TYPE Function]\n# @BRIEF Deletes a Superset environment.\n# @PRE: ID is provided.\n# @POST: Environment is removed from config.\n@router.delete(\"/environments/{id}\")\nasync def delete_environment(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"delete_environment\"):\n logger.reason(f\"Deleting environment {id}\", extra={\"src\": \"delete_environment\"})\n config_manager.delete_environment(id)\n return {\"message\": f\"Environment {id} deleted\"}\n\n\n# #endregion delete_environment\n\n\n# #region test_environment_connection [C:2] [TYPE Function]\n# @BRIEF Tests the connection to a Superset environment.\n# @PRE: ID is provided.\n# @POST: Returns success or error status.\n@router.post(\"/environments/{id}/test\")\nasync def test_environment_connection(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"test_environment_connection\"):\n logger.reason(f\"Testing environment {id}\", extra={\"src\": \"test_environment_connection\"})\n\n # Find environment\n env = next((e for e in config_manager.get_environments() if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n try:\n _validate_superset_connection_fast(env)\n\n logger.info(\n f\"[test_environment_connection][Coherence:OK] Connection successful for {id}\"\n )\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n except Exception as e:\n logger.error(\n f\"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}\"\n )\n return {\"status\": \"error\", \"message\": str(e)}\n\n\n# #endregion test_environment_connection\n\n\n# #region get_logging_config [C:2] [TYPE Function]\n# @BRIEF Retrieves current logging configuration.\n# @PRE: Config manager is available.\n# @POST: Returns logging configuration.\n@router.get(\"/logging\", response_model=LoggingConfigResponse)\nasync def get_logging_config(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_logging_config\"):\n logging_config = config_manager.get_config().settings.logging\n return LoggingConfigResponse(\n level=logging_config.level,\n task_log_level=logging_config.task_log_level,\n enable_belief_state=logging_config.enable_belief_state,\n )\n\n\n# #endregion get_logging_config\n\n\n# #region update_logging_config [C:2] [TYPE Function]\n# @BRIEF Updates logging configuration.\n# @PRE: New logging config is provided.\n# @POST: Logging configuration is updated and saved.\n@router.patch(\"/logging\", response_model=LoggingConfigResponse)\nasync def update_logging_config(\n config: LoggingConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_logging_config\"):\n logger.reason(\n f\"Updating logging config: level={config.level}, task_log_level={config.task_log_level}\", extra={\"src\": \"update_logging_config\"}\n )\n\n # Get current settings and update logging config\n settings = config_manager.get_config().settings\n settings.logging = config\n config_manager.update_global_settings(settings)\n\n return LoggingConfigResponse(\n level=config.level,\n task_log_level=config.task_log_level,\n enable_belief_state=config.enable_belief_state,\n )\n\n\n# #endregion update_logging_config\n\n\n# #region ConsolidatedSettingsResponse [C:1] [TYPE Class]\n# @BRIEF Response model for consolidated application settings.\nclass ConsolidatedSettingsResponse(BaseModel):\n environments: list[dict]\n connections: list[dict]\n llm: dict\n llm_providers: list[dict]\n logging: dict\n storage: dict\n notifications: dict = {}\n features: dict = {}\n\n\n# #endregion ConsolidatedSettingsResponse\n\n\n# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE: Config manager is available and the caller holds admin settings read permission.\n# @POST: Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT: Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT: Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": p.provider_type,\n \"name\": p.name,\n \"base_url\": p.base_url,\n \"api_key\": \"********\",\n \"default_model\": p.default_model,\n \"is_active\": p.is_active,\n }\n for p in providers\n ]\n\n config_record = (\n db.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n )\n if config_record and isinstance(config_record.payload, dict):\n notifications_payload = (\n config_record.payload.get(\"notifications\", {}) or {}\n )\n finally:\n db.close()\n\n normalized_llm = normalize_llm_settings(config.settings.llm)\n\n response_payload = ConsolidatedSettingsResponse(\n environments=[env.dict() for env in config.environments],\n connections=config.settings.connections,\n llm=normalized_llm,\n llm_providers=llm_providers_list,\n logging=config.settings.logging.dict(),\n storage=config.settings.storage.dict(),\n notifications=notifications_payload,\n features=config.settings.features.model_dump(),\n )\n logger.reflect(\n \"Consolidated settings payload assembled\",\n extra={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# #endregion get_consolidated_settings\n\n\n# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE: User has admin permissions, config is valid.\n# @POST: Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n\n\n# #region get_validation_policies [C:2] [TYPE Function]\n# @BRIEF Lists all validation policies.\n@router.get(\"/automation/policies\", response_model=list[ValidationPolicyResponse])\nasync def get_validation_policies(\n db: Session = Depends(get_db), _=Depends(has_permission(\"admin:settings\", \"READ\"))\n):\n with belief_scope(\"get_validation_policies\"):\n return db.query(ValidationPolicy).all()\n\n\n# #endregion get_validation_policies\n\n\n# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.dict())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n\n\n# #region update_validation_policy [C:2] [TYPE Function]\n# @BRIEF Updates an existing validation policy.\n@router.patch(\"/automation/policies/{id}\", response_model=ValidationPolicyResponse)\nasync def update_validation_policy(\n id: str,\n policy: ValidationPolicyUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n update_data = policy.dict(exclude_unset=True)\n for key, value in update_data.items():\n setattr(db_policy, key, value)\n\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion update_validation_policy\n\n\n# #region delete_validation_policy [C:2] [TYPE Function]\n# @BRIEF Deletes a validation policy.\n@router.delete(\"/automation/policies/{id}\")\nasync def delete_validation_policy(\n id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n db.delete(db_policy)\n db.commit()\n return {\"message\": \"Policy deleted\"}\n\n\n# #endregion delete_validation_policy\n\n\n# #region get_translation_schedules [C:2] [TYPE Function]\n# @BRIEF Lists all translation schedules joined with translation job names.\n@router.get(\"/automation/translation-schedules\", response_model=list[TranslationScheduleItem])\nasync def get_translation_schedules(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_translation_schedules\"):\n results = (\n db.query(\n TranslationSchedule,\n TranslationJob.name.label(\"job_name\"),\n )\n .outerjoin(TranslationJob, TranslationSchedule.job_id == TranslationJob.id)\n .all()\n )\n return [\n TranslationScheduleItem(\n schedule_id=ts.id,\n job_id=ts.job_id,\n job_name=job_name,\n cron_expression=ts.cron_expression,\n timezone=ts.timezone,\n is_active=ts.is_active,\n last_run_at=ts.last_run_at,\n next_run_at=ts.next_run_at,\n created_by=ts.created_by,\n created_at=ts.created_at,\n )\n for ts, job_name in results\n ]\n\n\n# #endregion get_translation_schedules\n\n# #endregion SettingsRouter\n" + "body": "# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response]\n#\n# @BRIEF Provides API endpoints for managing application settings and Superset environments.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_config_manager]\n# @RELATION DEPENDS_ON -> [has_permission]\n#\n# @PRE ConfigManager is initialized and accessible.\n# @POST Settings are read or written via ConfigManager.\n# @SIDE_EFFECT Persists config changes to disk via ConfigManager.\n# @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse\n# @INVARIANT: All settings changes must be persisted via ConfigManager.\n# @PUBLIC_API: router\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.config import AppConfigRecord\nfrom ...models.llm import ValidationPolicy\nfrom ...models.storage import StorageConfig\nfrom ...models.translate import TranslationJob, TranslationSchedule\nfrom ...schemas.settings import (\n TranslationScheduleItem,\n ValidationPolicyCreate,\n ValidationPolicyResponse,\n ValidationPolicyUpdate,\n)\nfrom ...services.llm_prompt_templates import normalize_llm_settings\n\n\n# #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response]\n# @BRIEF Response model for logging configuration with current task log level.\nclass LoggingConfigResponse(BaseModel):\n level: str\n task_log_level: str\n enable_belief_state: bool\n\n\n# #endregion LoggingConfigResponse\n\nrouter = APIRouter()\n\n\n# #region _normalize_superset_env_url [C:1] [TYPE Function]\n# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.\n# Auto-prepends https:// if no scheme is present.\n# @PRE: raw_url can be empty.\n# @POST: Returns normalized base URL with scheme.\ndef _normalize_superset_env_url(raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[: -len(\"/api/v1\")]\n normalized = normalized.rstrip(\"/\")\n # Auto-prepend https:// if no scheme is present\n if normalized and not normalized.startswith(\"http://\") and not normalized.startswith(\"https://\"):\n normalized = f\"https://{normalized}\"\n return normalized\n\n\n# #endregion _normalize_superset_env_url\n\n\n# #region _validate_superset_connection_fast [C:2] [TYPE Function]\n# @BRIEF Run lightweight Superset connectivity validation without full pagination scan.\n# @PRE: env contains valid URL and credentials.\n# @POST: Raises on auth/API failures; returns None on success.\ndef _validate_superset_connection_fast(env: Environment) -> None:\n client = SupersetClient(env)\n # 1) Explicit auth check\n client.authenticate()\n # 2) Single lightweight API call to ensure read access\n client.get_dashboards_page(\n query={\n \"page\": 0,\n \"page_size\": 1,\n \"columns\": [\"id\"],\n }\n )\n\n\n# #endregion _validate_superset_connection_fast\n\n\n# #region get_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves all application settings.\n# @PRE: Config manager is available.\n# @POST: Returns masked AppConfig.\n@router.get(\"\", response_model=AppConfig)\nasync def get_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_settings\"):\n logger.reason(\"Fetching all settings\", extra={\"src\": \"get_settings\"})\n config = config_manager.get_config().copy(deep=True)\n config.settings.llm = normalize_llm_settings(config.settings.llm)\n # Mask passwords\n for env in config.environments:\n if env.password:\n env.password = \"********\"\n return config\n\n\n# #endregion get_settings\n\n\n# #region get_features [C:1] [TYPE Function]\n# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering.\n# @RATIONALE: Unauthenticated because sidebar filtering must work for all users, not just admins.\n# @PRE: Config manager is available.\n# @POST: Returns dict with dataset_review and health_monitor booleans.\n@router.get(\"/features\")\nasync def get_features(\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n return config_manager.get_config().settings.features.model_dump()\n\n\n# #endregion get_features\n\n\n# #region update_global_settings [C:2] [TYPE Function]\n# @BRIEF Updates global application settings.\n# @PRE: New settings are provided.\n# @POST: Global settings are updated.\n@router.patch(\"/global\", response_model=GlobalSettings)\nasync def update_global_settings(\n settings: GlobalSettings,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_global_settings\"):\n logger.reason(\"Updating global settings\", extra={\"src\": \"update_global_settings\"})\n\n config_manager.update_global_settings(settings)\n return settings\n\n\n# #endregion update_global_settings\n\n\n# #region get_storage_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves storage-specific settings.\n@router.get(\"/storage\", response_model=StorageConfig)\nasync def get_storage_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_storage_settings\"):\n return config_manager.get_config().settings.storage\n\n\n# #endregion get_storage_settings\n\n\n# #region update_storage_settings [C:2] [TYPE Function]\n# @BRIEF Updates storage-specific settings.\n# @POST: Storage settings are updated and saved.\n@router.put(\"/storage\", response_model=StorageConfig)\nasync def update_storage_settings(\n storage: StorageConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_storage_settings\"):\n is_valid, message = config_manager.validate_path(storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n\n settings = config_manager.get_config().settings\n settings.storage = storage\n config_manager.update_global_settings(settings)\n return config_manager.get_config().settings.storage\n\n\n# #endregion update_storage_settings\n\n\n# #region get_environments [C:2] [TYPE Function]\n# @BRIEF Lists all configured Superset environments.\n# @PRE: Config manager is available.\n# @POST: Returns list of environments.\n@router.get(\"/environments\", response_model=list[Environment])\nasync def get_environments(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n logger.reason(\"Fetching environments\", extra={\"src\": \"get_environments\"})\n environments = config_manager.get_environments()\n return [\n env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n for env in environments\n ]\n\n\n# #endregion get_environments\n\n\n# #region add_environment [C:2] [TYPE Function]\n# @BRIEF Adds a new Superset environment.\n# @PRE: Environment data is valid and reachable.\n# @POST: Environment is added to config.\n@router.post(\"/environments\", response_model=Environment)\nasync def add_environment(\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"add_environment\"):\n logger.reason(f\"Adding environment {env.id}\", extra={\"src\": \"add_environment\"})\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # Validate connection before adding (fast path)\n try:\n _validate_superset_connection_fast(env)\n except Exception as e:\n logger.error(\n f\"[add_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n config_manager.add_environment(env)\n return env\n\n\n# #endregion add_environment\n\n\n# #region update_environment [C:2] [TYPE Function]\n# @BRIEF Updates an existing Superset environment.\n# @PRE: ID and valid environment data are provided.\n# @POST: Environment is updated in config.\n@router.put(\"/environments/{id}\", response_model=Environment)\nasync def update_environment(\n id: str,\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n with belief_scope(\"update_environment\"):\n logger.reason(f\"Updating environment {id}\", extra={\"src\": \"update_environment\"})\n\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # If password is masked, we need the real one for validation\n env_to_validate = env.copy(deep=True)\n if env_to_validate.password == \"********\":\n old_env = next(\n (e for e in config_manager.get_environments() if e.id == id), None\n )\n if old_env:\n env_to_validate.password = old_env.password\n\n # Validate connection before updating (fast path)\n try:\n _validate_superset_connection_fast(env_to_validate)\n except Exception as e:\n logger.error(\n f\"[update_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n if config_manager.update_environment(id, env):\n return env\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n\n# #endregion update_environment\n\n\n# #region delete_environment [C:2] [TYPE Function]\n# @BRIEF Deletes a Superset environment.\n# @PRE: ID is provided.\n# @POST: Environment is removed from config.\n@router.delete(\"/environments/{id}\")\nasync def delete_environment(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"delete_environment\"):\n logger.reason(f\"Deleting environment {id}\", extra={\"src\": \"delete_environment\"})\n config_manager.delete_environment(id)\n return {\"message\": f\"Environment {id} deleted\"}\n\n\n# #endregion delete_environment\n\n\n# #region test_environment_connection [C:2] [TYPE Function]\n# @BRIEF Tests the connection to a Superset environment.\n# @PRE: ID is provided.\n# @POST: Returns success or error status.\n@router.post(\"/environments/{id}/test\")\nasync def test_environment_connection(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"test_environment_connection\"):\n logger.reason(f\"Testing environment {id}\", extra={\"src\": \"test_environment_connection\"})\n\n # Find environment\n env = next((e for e in config_manager.get_environments() if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n try:\n _validate_superset_connection_fast(env)\n\n logger.info(\n f\"[test_environment_connection][Coherence:OK] Connection successful for {id}\"\n )\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n except Exception as e:\n logger.error(\n f\"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}\"\n )\n return {\"status\": \"error\", \"message\": str(e)}\n\n\n# #endregion test_environment_connection\n\n\n# #region get_logging_config [C:2] [TYPE Function]\n# @BRIEF Retrieves current logging configuration.\n# @PRE: Config manager is available.\n# @POST: Returns logging configuration.\n@router.get(\"/logging\", response_model=LoggingConfigResponse)\nasync def get_logging_config(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_logging_config\"):\n logging_config = config_manager.get_config().settings.logging\n return LoggingConfigResponse(\n level=logging_config.level,\n task_log_level=logging_config.task_log_level,\n enable_belief_state=logging_config.enable_belief_state,\n )\n\n\n# #endregion get_logging_config\n\n\n# #region update_logging_config [C:2] [TYPE Function]\n# @BRIEF Updates logging configuration.\n# @PRE: New logging config is provided.\n# @POST: Logging configuration is updated and saved.\n@router.patch(\"/logging\", response_model=LoggingConfigResponse)\nasync def update_logging_config(\n config: LoggingConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_logging_config\"):\n logger.reason(\n f\"Updating logging config: level={config.level}, task_log_level={config.task_log_level}\", extra={\"src\": \"update_logging_config\"}\n )\n\n # Get current settings and update logging config\n settings = config_manager.get_config().settings\n settings.logging = config\n config_manager.update_global_settings(settings)\n\n return LoggingConfigResponse(\n level=config.level,\n task_log_level=config.task_log_level,\n enable_belief_state=config.enable_belief_state,\n )\n\n\n# #endregion update_logging_config\n\n\n# #region ConsolidatedSettingsResponse [C:1] [TYPE Class]\n# @BRIEF Response model for consolidated application settings.\nclass ConsolidatedSettingsResponse(BaseModel):\n environments: list[dict]\n connections: list[dict]\n llm: dict\n llm_providers: list[dict]\n logging: dict\n storage: dict\n notifications: dict = {}\n features: dict = {}\n\n\n# #endregion ConsolidatedSettingsResponse\n\n\n# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE: Config manager is available and the caller holds admin settings read permission.\n# @POST: Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT: Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT: Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": p.provider_type,\n \"name\": p.name,\n \"base_url\": p.base_url,\n \"api_key\": \"********\",\n \"default_model\": p.default_model,\n \"is_active\": p.is_active,\n }\n for p in providers\n ]\n\n config_record = (\n db.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n )\n if config_record and isinstance(config_record.payload, dict):\n notifications_payload = (\n config_record.payload.get(\"notifications\", {}) or {}\n )\n finally:\n db.close()\n\n normalized_llm = normalize_llm_settings(config.settings.llm)\n\n response_payload = ConsolidatedSettingsResponse(\n environments=[env.dict() for env in config.environments],\n connections=config.settings.connections,\n llm=normalized_llm,\n llm_providers=llm_providers_list,\n logging=config.settings.logging.dict(),\n storage=config.settings.storage.dict(),\n notifications=notifications_payload,\n features=config.settings.features.model_dump(),\n )\n logger.reflect(\n \"Consolidated settings payload assembled\",\n extra={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# #endregion get_consolidated_settings\n\n\n# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE: User has admin permissions, config is valid.\n# @POST: Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n\n\n# #region get_validation_policies [C:2] [TYPE Function]\n# @BRIEF Lists all validation policies.\n@router.get(\"/automation/policies\", response_model=list[ValidationPolicyResponse])\nasync def get_validation_policies(\n db: Session = Depends(get_db), _=Depends(has_permission(\"admin:settings\", \"READ\"))\n):\n with belief_scope(\"get_validation_policies\"):\n return db.query(ValidationPolicy).all()\n\n\n# #endregion get_validation_policies\n\n\n# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.dict())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n\n\n# #region update_validation_policy [C:2] [TYPE Function]\n# @BRIEF Updates an existing validation policy.\n@router.patch(\"/automation/policies/{id}\", response_model=ValidationPolicyResponse)\nasync def update_validation_policy(\n id: str,\n policy: ValidationPolicyUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n update_data = policy.dict(exclude_unset=True)\n for key, value in update_data.items():\n setattr(db_policy, key, value)\n\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion update_validation_policy\n\n\n# #region delete_validation_policy [C:2] [TYPE Function]\n# @BRIEF Deletes a validation policy.\n@router.delete(\"/automation/policies/{id}\")\nasync def delete_validation_policy(\n id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n db.delete(db_policy)\n db.commit()\n return {\"message\": \"Policy deleted\"}\n\n\n# #endregion delete_validation_policy\n\n\n# #region get_translation_schedules [C:2] [TYPE Function]\n# @BRIEF Lists all translation schedules joined with translation job names.\n@router.get(\"/automation/translation-schedules\", response_model=list[TranslationScheduleItem])\nasync def get_translation_schedules(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_translation_schedules\"):\n results = (\n db.query(\n TranslationSchedule,\n TranslationJob.name.label(\"job_name\"),\n )\n .outerjoin(TranslationJob, TranslationSchedule.job_id == TranslationJob.id)\n .all()\n )\n return [\n TranslationScheduleItem(\n schedule_id=ts.id,\n job_id=ts.job_id,\n job_name=job_name,\n cron_expression=ts.cron_expression,\n timezone=ts.timezone,\n is_active=ts.is_active,\n last_run_at=ts.last_run_at,\n next_run_at=ts.next_run_at,\n created_by=ts.created_by,\n created_at=ts.created_at,\n )\n for ts, job_name in results\n ]\n\n\n# #endregion get_translation_schedules\n\n# #endregion SettingsRouter\n" }, { "contract_id": "LoggingConfigResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/settings.py", - "start_line": 36, - "end_line": 44, + "start_line": 40, + "end_line": 48, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20382,8 +20296,8 @@ "contract_id": "_validate_superset_connection_fast", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 63, - "end_line": 81, + "start_line": 72, + "end_line": 90, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20421,8 +20335,8 @@ "contract_id": "get_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 84, - "end_line": 104, + "start_line": 93, + "end_line": 113, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20460,8 +20374,8 @@ "contract_id": "get_features", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 107, - "end_line": 119, + "start_line": 116, + "end_line": 128, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20500,8 +20414,8 @@ "contract_id": "get_storage_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 142, - "end_line": 153, + "start_line": 151, + "end_line": 162, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20518,8 +20432,8 @@ "contract_id": "update_storage_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 156, - "end_line": 176, + "start_line": 165, + "end_line": 185, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20547,8 +20461,8 @@ "contract_id": "test_environment_connection", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 292, - "end_line": 322, + "start_line": 301, + "end_line": 331, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20586,8 +20500,8 @@ "contract_id": "get_logging_config", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 325, - "end_line": 343, + "start_line": 334, + "end_line": 352, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20625,8 +20539,8 @@ "contract_id": "update_logging_config", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 346, - "end_line": 373, + "start_line": 355, + "end_line": 382, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20664,8 +20578,8 @@ "contract_id": "ConsolidatedSettingsResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/settings.py", - "start_line": 376, - "end_line": 389, + "start_line": 385, + "end_line": 398, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20682,8 +20596,8 @@ "contract_id": "get_consolidated_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 392, - "end_line": 467, + "start_line": 401, + "end_line": 476, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -20751,8 +20665,8 @@ "contract_id": "update_consolidated_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 470, - "end_line": 523, + "start_line": 479, + "end_line": 532, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20790,8 +20704,8 @@ "contract_id": "get_validation_policies", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 526, - "end_line": 536, + "start_line": 535, + "end_line": 545, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20808,8 +20722,8 @@ "contract_id": "create_validation_policy", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 539, - "end_line": 555, + "start_line": 548, + "end_line": 564, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20826,8 +20740,8 @@ "contract_id": "update_validation_policy", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 558, - "end_line": 581, + "start_line": 567, + "end_line": 590, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20844,8 +20758,8 @@ "contract_id": "delete_validation_policy", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 584, - "end_line": 602, + "start_line": 593, + "end_line": 611, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20862,8 +20776,8 @@ "contract_id": "get_translation_schedules", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 605, - "end_line": 638, + "start_line": 614, + "end_line": 647, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -20882,10 +20796,10 @@ "file_path": "backend/src/api/routes/storage.py", "start_line": 1, "end_line": 170, - "tier": "TIER_2", - "complexity": 3, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "INVARIANT": "All paths must be validated against path traversal.", "LAYER": "API", "PURPOSE": "API endpoints for file storage management (backups and repositories)." @@ -20906,18 +20820,45 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region storage_routes [C:3] [TYPE Module] [SEMANTICS fastapi, storage, api, upload, download]\n#\n# @BRIEF API endpoints for file storage management (backups and repositories).\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [StorageModels]\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n#\n# @INVARIANT: All paths must be validated against path traversal.\n\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile\nfrom fastapi.responses import FileResponse\n\nfrom ...core.logger import belief_scope\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...models.storage import FileCategory, StoredFile\nfrom ...plugins.storage.plugin import StoragePlugin\n\nrouter = APIRouter(tags=[\"storage\"])\n\n# #region list_files [C:3] [TYPE Function]\n# @BRIEF List all files and directories in the storage system.\n#\n# @PRE: None.\n# @POST: Returns a list of StoredFile objects.\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/files\", response_model=list[StoredFile])\nasync def list_files(\n category: FileCategory | None = None,\n path: str | None = None,\n recursive: bool = False,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"READ\"))\n):\n with belief_scope(\"list_files\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n return storage_plugin.list_files(category, path, recursive)\n# #endregion list_files\n\n# #region upload_file [C:3] [TYPE Function]\n# @BRIEF Upload a file to the storage system.\n#\n# @PRE: category must be a valid FileCategory.\n# @PRE: file must be a valid UploadFile.\n# @POST: Returns the StoredFile object of the uploaded file.\n#\n#\n# @SIDE_EFFECT: Writes file to the filesystem.\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.post(\"/upload\", response_model=StoredFile, status_code=201)\nasync def upload_file(\n category: FileCategory = Form(...),\n path: str | None = Form(None),\n file: UploadFile = File(...),\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"WRITE\"))\n):\n with belief_scope(\"upload_file\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n try:\n return await storage_plugin.save_file(file, category, path)\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n# #endregion upload_file\n\n# #region delete_file [C:3] [TYPE Function]\n# @BRIEF Delete a specific file or directory.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Item is removed from storage.\n#\n#\n# @SIDE_EFFECT: Deletes item from the filesystem.\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.delete(\"/files/{category}/{path:path}\", status_code=204)\nasync def delete_file(\n category: FileCategory,\n path: str,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"WRITE\"))\n):\n with belief_scope(\"delete_file\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n try:\n storage_plugin.delete_file(category, path)\n except FileNotFoundError:\n raise HTTPException(status_code=404, detail=\"File not found\")\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n# #endregion delete_file\n\n# #region download_file [C:3] [TYPE Function]\n# @BRIEF Retrieve a file for download.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Returns a FileResponse.\n#\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/download/{category}/{path:path}\")\nasync def download_file(\n category: FileCategory,\n path: str,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"READ\"))\n):\n with belief_scope(\"download_file\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n try:\n abs_path = storage_plugin.get_file_path(category, path)\n filename = Path(path).name\n return FileResponse(path=abs_path, filename=filename)\n except FileNotFoundError:\n raise HTTPException(status_code=404, detail=\"File not found\")\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n# #endregion download_file\n\n# #region get_file_by_path [C:3] [TYPE Function]\n# @BRIEF Retrieve a file by validated absolute/relative path under storage root.\n#\n# @PRE: path must resolve under configured storage root.\n# @POST: Returns a FileResponse for existing files.\n#\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/file\")\nasync def get_file_by_path(\n path: str,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"READ\"))\n):\n with belief_scope(\"get_file_by_path\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n\n requested_path = (path or \"\").strip()\n if not requested_path:\n raise HTTPException(status_code=400, detail=\"Path is required\")\n\n try:\n candidate = Path(requested_path)\n if candidate.is_absolute():\n abs_path = storage_plugin.validate_path(candidate)\n else:\n storage_root = storage_plugin.get_storage_root()\n abs_path = storage_plugin.validate_path(storage_root / candidate)\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n\n if not abs_path.exists() or not abs_path.is_file():\n raise HTTPException(status_code=404, detail=\"File not found\")\n\n return FileResponse(path=str(abs_path), filename=abs_path.name)\n# #endregion get_file_by_path\n\n# #endregion storage_routes\n" + "body": "# #region storage_routes [C:5] [TYPE Module] [SEMANTICS fastapi, storage, api, upload, download]\n#\n# @BRIEF API endpoints for file storage management (backups and repositories).\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [StorageModels]\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n#\n# @INVARIANT: All paths must be validated against path traversal.\n\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile\nfrom fastapi.responses import FileResponse\n\nfrom ...core.logger import belief_scope\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...models.storage import FileCategory, StoredFile\nfrom ...plugins.storage.plugin import StoragePlugin\n\nrouter = APIRouter(tags=[\"storage\"])\n\n# #region list_files [C:3] [TYPE Function]\n# @BRIEF List all files and directories in the storage system.\n#\n# @PRE: None.\n# @POST: Returns a list of StoredFile objects.\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/files\", response_model=list[StoredFile])\nasync def list_files(\n category: FileCategory | None = None,\n path: str | None = None,\n recursive: bool = False,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"READ\"))\n):\n with belief_scope(\"list_files\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n return storage_plugin.list_files(category, path, recursive)\n# #endregion list_files\n\n# #region upload_file [C:3] [TYPE Function]\n# @BRIEF Upload a file to the storage system.\n#\n# @PRE: category must be a valid FileCategory.\n# @PRE: file must be a valid UploadFile.\n# @POST: Returns the StoredFile object of the uploaded file.\n#\n#\n# @SIDE_EFFECT: Writes file to the filesystem.\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.post(\"/upload\", response_model=StoredFile, status_code=201)\nasync def upload_file(\n category: FileCategory = Form(...),\n path: str | None = Form(None),\n file: UploadFile = File(...),\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"WRITE\"))\n):\n with belief_scope(\"upload_file\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n try:\n return await storage_plugin.save_file(file, category, path)\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n# #endregion upload_file\n\n# #region delete_file [C:3] [TYPE Function]\n# @BRIEF Delete a specific file or directory.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Item is removed from storage.\n#\n#\n# @SIDE_EFFECT: Deletes item from the filesystem.\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.delete(\"/files/{category}/{path:path}\", status_code=204)\nasync def delete_file(\n category: FileCategory,\n path: str,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"WRITE\"))\n):\n with belief_scope(\"delete_file\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n try:\n storage_plugin.delete_file(category, path)\n except FileNotFoundError:\n raise HTTPException(status_code=404, detail=\"File not found\")\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n# #endregion delete_file\n\n# #region download_file [C:3] [TYPE Function]\n# @BRIEF Retrieve a file for download.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Returns a FileResponse.\n#\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/download/{category}/{path:path}\")\nasync def download_file(\n category: FileCategory,\n path: str,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"READ\"))\n):\n with belief_scope(\"download_file\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n try:\n abs_path = storage_plugin.get_file_path(category, path)\n filename = Path(path).name\n return FileResponse(path=abs_path, filename=filename)\n except FileNotFoundError:\n raise HTTPException(status_code=404, detail=\"File not found\")\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n# #endregion download_file\n\n# #region get_file_by_path [C:3] [TYPE Function]\n# @BRIEF Retrieve a file by validated absolute/relative path under storage root.\n#\n# @PRE: path must resolve under configured storage root.\n# @POST: Returns a FileResponse for existing files.\n#\n#\n# @RELATION DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/file\")\nasync def get_file_by_path(\n path: str,\n plugin_loader=Depends(get_plugin_loader),\n _ = Depends(has_permission(\"plugin:storage\", \"READ\"))\n):\n with belief_scope(\"get_file_by_path\"):\n storage_plugin: StoragePlugin = plugin_loader.get_plugin(\"storage-manager\")\n if not storage_plugin:\n raise HTTPException(status_code=500, detail=\"Storage plugin not loaded\")\n\n requested_path = (path or \"\").strip()\n if not requested_path:\n raise HTTPException(status_code=400, detail=\"Path is required\")\n\n try:\n candidate = Path(requested_path)\n if candidate.is_absolute():\n abs_path = storage_plugin.validate_path(candidate)\n else:\n storage_root = storage_plugin.get_storage_root()\n abs_path = storage_plugin.validate_path(storage_root / candidate)\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n\n if not abs_path.exists() or not abs_path.is_file():\n raise HTTPException(status_code=404, detail=\"File not found\")\n\n return FileResponse(path=str(abs_path), filename=abs_path.name)\n# #endregion get_file_by_path\n\n# #endregion storage_routes\n" }, { "contract_id": "list_files", @@ -21123,7 +21064,7 @@ "complexity": 3, "metadata": { "COMPLEXITY": 3, - "LAYER": "UI (API)", + "LAYER": "API", "PURPOSE": "Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks." }, "relations": [ @@ -21146,86 +21087,10 @@ "target_ref": "[LLMProviderService]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TasksRouter [C:3] [TYPE Module] [SEMANTICS fastapi, task, api, search, create-task-request, resolve-task-request]\n# @BRIEF Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n\nfrom typing import Any\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom pydantic import BaseModel\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager import Task, TaskManager, TaskStatus\nfrom ...core.task_manager.models import LogFilter, LogStats\nfrom ...dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n has_permission,\n)\nfrom ...services.llm_prompt_templates import (\n is_multimodal_model,\n normalize_llm_settings,\n resolve_bound_provider_id,\n)\n\nrouter = APIRouter()\n\nTASK_TYPE_PLUGIN_MAP = {\n \"llm_validation\": [\"llm_dashboard_validation\"],\n \"backup\": [\"superset-backup\"],\n \"migration\": [\"superset-migration\"],\n}\n\n\nclass CreateTaskRequest(BaseModel):\n plugin_id: str\n params: dict[str, Any]\n\n\nclass ResolveTaskRequest(BaseModel):\n resolution_params: dict[str, Any]\n\n\nclass ResumeTaskRequest(BaseModel):\n passwords: dict[str, str]\n\n\n# #region create_task [C:3] [TYPE Function]\n# @BRIEF Create and start a new task for a given plugin.\n# @PRE: plugin_id must exist and params must be valid for that plugin.\n# @POST: A new task is created and started.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n@router.post(\"\", response_model=Task, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n request: CreateTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n current_user=Depends(get_current_user),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n # Dynamic permission check based on plugin_id\n has_permission(f\"plugin:{request.plugin_id}\", \"EXECUTE\")(current_user)\n with belief_scope(\"create_task\"):\n try:\n # Special handling for LLM tasks to resolve provider config by task binding.\n if request.plugin_id in {\"llm_dashboard_validation\", \"llm_documentation\"}:\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n try:\n llm_service = LLMProviderService(db)\n provider_id = request.params.get(\"provider_id\")\n if not provider_id:\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n binding_key = (\n \"dashboard_validation\"\n if request.plugin_id == \"llm_dashboard_validation\"\n else \"documentation\"\n )\n provider_id = resolve_bound_provider_id(\n llm_settings, binding_key\n )\n if provider_id:\n request.params[\"provider_id\"] = provider_id\n if not provider_id:\n providers = llm_service.get_all_providers()\n active_provider = next(\n (p for p in providers if p.is_active), None\n )\n if active_provider:\n provider_id = active_provider.id\n request.params[\"provider_id\"] = provider_id\n\n if provider_id:\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n if (\n request.plugin_id == \"llm_dashboard_validation\"\n and not is_multimodal_model(\n db_provider.default_model,\n db_provider.provider_type,\n )\n ):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"Selected provider model is not multimodal for dashboard validation\",\n )\n finally:\n db.close()\n\n task = await task_manager.create_task(\n plugin_id=request.plugin_id, params=request.params\n )\n return task\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n\n\n# #endregion create_task\n\n\n# #region list_tasks [C:2] [TYPE Function]\n# @BRIEF Retrieve a list of tasks with pagination and optional status filter.\n# @PRE: task_manager must be available.\n# @POST: Returns a list of tasks.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]\n@router.get(\"\", response_model=list[Task])\nasync def list_tasks(\n limit: int = 10,\n offset: int = 0,\n status_filter: TaskStatus | None = Query(None, alias=\"status\"),\n task_type: str | None = Query(\n None, description=\"Task category: llm_validation, backup, migration\"\n ),\n plugin_id: list[str] | None = Query(\n None, description=\"Filter by plugin_id (repeatable query param)\"\n ),\n completed_only: bool = Query(\n False, description=\"Return only completed tasks (SUCCESS/FAILED)\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_tasks\"):\n plugin_filters = list(plugin_id) if plugin_id else []\n if task_type:\n if task_type not in TASK_TYPE_PLUGIN_MAP:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}\",\n )\n plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])\n\n return task_manager.get_tasks(\n limit=limit,\n offset=offset,\n status=status_filter,\n plugin_ids=plugin_filters or None,\n completed_only=completed_only,\n )\n\n\n# #endregion list_tasks\n\n\n# #region get_task [C:2] [TYPE Function]\n# @BRIEF Retrieve the details of a specific task.\n# @PRE: task_id must exist.\n# @POST: Returns task details or raises 404.\n# @RELATION CALLS -> [TaskManager]\n@router.get(\"/{task_id}\", response_model=Task)\nasync def get_task(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_task\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task\n\n\n# #endregion get_task\n\n\n# #region get_task_logs [C:5] [TYPE Function]\n# @BRIEF Retrieve logs for a specific task with optional filtering.\n# @PRE: task_id must exist.\n# @POST: Returns a list of log entries or raises 404.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [LogFilter]\n# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]\n# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.\n# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json\n# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.\n# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.\n# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.\n# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]\n@router.get(\"/{task_id}/logs\")\nasync def get_task_logs(\n task_id: str,\n level: str | None = Query(\n None, description=\"Filter by log level (DEBUG, INFO, WARNING, ERROR)\"\n ),\n source: str | None = Query(None, description=\"Filter by source component\"),\n search: str | None = Query(None, description=\"Text search in message\"),\n offset: int = Query(0, ge=0, description=\"Number of logs to skip\"),\n limit: int = Query(\n 100, ge=1, le=1000, description=\"Maximum number of logs to return\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_logs\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n\n log_filter = LogFilter(\n level=level.upper() if level else None,\n source=source,\n search=search,\n offset=offset,\n limit=limit,\n )\n return task_manager.get_task_logs(task_id, log_filter)\n\n\n# #endregion get_task_logs\n\n\n# #region get_task_log_stats [C:2] [TYPE Function]\n# @BRIEF Get statistics about logs for a task (counts by level and source).\n# @PRE: task_id must exist.\n# @POST: Returns log statistics or raises 404.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [LogStats]\n@router.get(\"/{task_id}/logs/stats\", response_model=LogStats)\nasync def get_task_log_stats(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_stats\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n stats_payload = task_manager.get_task_log_stats(task_id)\n if isinstance(stats_payload, LogStats):\n return stats_payload\n if isinstance(stats_payload, dict) and (\n \"total_count\" in stats_payload\n or \"by_level\" in stats_payload\n or \"by_source\" in stats_payload\n ):\n return LogStats(\n total_count=int(stats_payload.get(\"total_count\", 0) or 0),\n by_level=dict(stats_payload.get(\"by_level\") or {}),\n by_source=dict(stats_payload.get(\"by_source\") or {}),\n )\n flat_by_level = (\n dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}\n )\n return LogStats(\n total_count=sum(int(value or 0) for value in flat_by_level.values()),\n by_level={\n str(key): int(value or 0) for key, value in flat_by_level.items()\n },\n by_source={},\n )\n\n\n# #endregion get_task_log_stats\n\n\n# #region get_task_log_sources [C:2] [TYPE Function]\n# @BRIEF Get unique sources for a task's logs.\n# @PRE: task_id must exist.\n# @POST: Returns list of unique source names or raises 404.\n# @RELATION CALLS -> [TaskManager]\n@router.get(\"/{task_id}/logs/sources\", response_model=list[str])\nasync def get_task_log_sources(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_sources\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task_manager.get_task_log_sources(task_id)\n\n\n# #endregion get_task_log_sources\n\n\n# #region resolve_task [C:2] [TYPE Function]\n# @BRIEF Resolve a task that is awaiting mapping.\n# @PRE: task must be in AWAITING_MAPPING status.\n# @POST: Task is resolved and resumes execution.\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resolve\", response_model=Task)\nasync def resolve_task(\n task_id: str,\n request: ResolveTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resolve_task\"):\n try:\n await task_manager.resolve_task(task_id, request.resolution_params)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# #endregion resolve_task\n\n\n# #region resume_task [C:2] [TYPE Function]\n# @BRIEF Resume a task that is awaiting input (e.g., passwords).\n# @PRE: task must be in AWAITING_INPUT status.\n# @POST: Task resumes execution with provided input.\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resume\", response_model=Task)\nasync def resume_task(\n task_id: str,\n request: ResumeTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resume_task\"):\n try:\n task_manager.resume_task_with_password(task_id, request.passwords)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# #endregion resume_task\n\n\n# #region clear_tasks [C:2] [TYPE Function]\n# @BRIEF Clear tasks matching the status filter.\n# @PRE: task_manager is available.\n# @POST: Tasks are removed from memory/persistence.\n# @RELATION CALLS -> [TaskManager]\n@router.delete(\"\", status_code=status.HTTP_204_NO_CONTENT)\nasync def clear_tasks(\n status: TaskStatus | None = None,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"clear_tasks\", f\"status={status}\"):\n task_manager.clear_tasks(status)\n return\n\n\n# #endregion clear_tasks\n\n# #endregion TasksRouter\n" - }, - { - "contract_id": "create_task", - "contract_type": "Function", - "file_path": "backend/src/api/routes/tasks.py", - "start_line": 51, - "end_line": 127, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "POST": "A new task is created and started.", - "PRE": "plugin_id must exist and params must be valid for that plugin.", - "PURPOSE": "Create and start a new task for a given plugin." - }, - "relations": [ - { - "source_id": "create_task", - "relation_type": "CALLS", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - }, - { - "source_id": "create_task", - "relation_type": "DEPENDS_ON", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" - }, - { - "source_id": "create_task", - "relation_type": "DEPENDS_ON", - "target_id": "LLMProviderService", - "target_ref": "[LLMProviderService]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region create_task [C:3] [TYPE Function]\n# @BRIEF Create and start a new task for a given plugin.\n# @PRE: plugin_id must exist and params must be valid for that plugin.\n# @POST: A new task is created and started.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n@router.post(\"\", response_model=Task, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n request: CreateTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n current_user=Depends(get_current_user),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n # Dynamic permission check based on plugin_id\n has_permission(f\"plugin:{request.plugin_id}\", \"EXECUTE\")(current_user)\n with belief_scope(\"create_task\"):\n try:\n # Special handling for LLM tasks to resolve provider config by task binding.\n if request.plugin_id in {\"llm_dashboard_validation\", \"llm_documentation\"}:\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n try:\n llm_service = LLMProviderService(db)\n provider_id = request.params.get(\"provider_id\")\n if not provider_id:\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n binding_key = (\n \"dashboard_validation\"\n if request.plugin_id == \"llm_dashboard_validation\"\n else \"documentation\"\n )\n provider_id = resolve_bound_provider_id(\n llm_settings, binding_key\n )\n if provider_id:\n request.params[\"provider_id\"] = provider_id\n if not provider_id:\n providers = llm_service.get_all_providers()\n active_provider = next(\n (p for p in providers if p.is_active), None\n )\n if active_provider:\n provider_id = active_provider.id\n request.params[\"provider_id\"] = provider_id\n\n if provider_id:\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n if (\n request.plugin_id == \"llm_dashboard_validation\"\n and not is_multimodal_model(\n db_provider.default_model,\n db_provider.provider_type,\n )\n ):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"Selected provider model is not multimodal for dashboard validation\",\n )\n finally:\n db.close()\n\n task = await task_manager.create_task(\n plugin_id=request.plugin_id, params=request.params\n )\n return task\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n\n\n# #endregion create_task\n" + "body": "# #region TasksRouter [C:3] [TYPE Module] [SEMANTICS fastapi, task, api, search, create-task-request, resolve-task-request]\n# @BRIEF Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n\nfrom typing import Any\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom pydantic import BaseModel\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager import Task, TaskManager, TaskStatus\nfrom ...core.task_manager.models import LogFilter, LogStats\nfrom ...dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n has_permission,\n)\nfrom ...services.llm_prompt_templates import (\n is_multimodal_model,\n normalize_llm_settings,\n resolve_bound_provider_id,\n)\n\nrouter = APIRouter()\n\nTASK_TYPE_PLUGIN_MAP = {\n \"llm_validation\": [\"llm_dashboard_validation\"],\n \"backup\": [\"superset-backup\"],\n \"migration\": [\"superset-migration\"],\n}\n\n\nclass CreateTaskRequest(BaseModel):\n plugin_id: str\n params: dict[str, Any]\n\n\nclass ResolveTaskRequest(BaseModel):\n resolution_params: dict[str, Any]\n\n\nclass ResumeTaskRequest(BaseModel):\n passwords: dict[str, str]\n\n\n# #region create_task [C:3] [TYPE Function]\n# @BRIEF Create and start a new task for a given plugin.\n# @PRE: plugin_id must exist and params must be valid for that plugin.\n# @POST: A new task is created and started.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n@router.post(\"\", response_model=Task, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n request: CreateTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n current_user=Depends(get_current_user),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n # Dynamic permission check based on plugin_id\n has_permission(f\"plugin:{request.plugin_id}\", \"EXECUTE\")(current_user)\n with belief_scope(\"create_task\"):\n try:\n # Special handling for LLM tasks to resolve provider config by task binding.\n if request.plugin_id in {\"llm_dashboard_validation\", \"llm_documentation\"}:\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n try:\n llm_service = LLMProviderService(db)\n provider_id = request.params.get(\"provider_id\")\n if not provider_id:\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n binding_key = (\n \"dashboard_validation\"\n if request.plugin_id == \"llm_dashboard_validation\"\n else \"documentation\"\n )\n provider_id = resolve_bound_provider_id(\n llm_settings, binding_key\n )\n if provider_id:\n request.params[\"provider_id\"] = provider_id\n if not provider_id:\n providers = llm_service.get_all_providers()\n active_provider = next(\n (p for p in providers if p.is_active), None\n )\n if active_provider:\n provider_id = active_provider.id\n request.params[\"provider_id\"] = provider_id\n\n if provider_id:\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n if (\n request.plugin_id == \"llm_dashboard_validation\"\n and not is_multimodal_model(\n db_provider.default_model,\n db_provider.provider_type,\n )\n ):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"Selected provider model is not multimodal for dashboard validation\",\n )\n finally:\n db.close()\n\n task = await task_manager.create_task(\n plugin_id=request.plugin_id, params=request.params\n )\n return task\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n\n\n# #endregion create_task\n\n\n# #region list_tasks [C:2] [TYPE Function]\n# @BRIEF Retrieve a list of tasks with pagination and optional status filter.\n# @PRE: task_manager must be available.\n# @POST: Returns a list of tasks.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]\n@router.get(\"\", response_model=list[Task])\nasync def list_tasks(\n limit: int = 10,\n offset: int = 0,\n status_filter: TaskStatus | None = Query(None, alias=\"status\"),\n task_type: str | None = Query(\n None, description=\"Task category: llm_validation, backup, migration\"\n ),\n plugin_id: list[str] | None = Query(\n None, description=\"Filter by plugin_id (repeatable query param)\"\n ),\n completed_only: bool = Query(\n False, description=\"Return only completed tasks (SUCCESS/FAILED)\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_tasks\"):\n plugin_filters = list(plugin_id) if plugin_id else []\n if task_type:\n if task_type not in TASK_TYPE_PLUGIN_MAP:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}\",\n )\n plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])\n\n return task_manager.get_tasks(\n limit=limit,\n offset=offset,\n status=status_filter,\n plugin_ids=plugin_filters or None,\n completed_only=completed_only,\n )\n\n\n# #endregion list_tasks\n\n\n# #region get_task [C:2] [TYPE Function]\n# @BRIEF Retrieve the details of a specific task.\n# @PRE: task_id must exist.\n# @POST: Returns task details or raises 404.\n# @RELATION CALLS -> [TaskManager]\n@router.get(\"/{task_id}\", response_model=Task)\nasync def get_task(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_task\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task\n\n\n# #endregion get_task\n\n\n# #region get_task_logs [C:5] [TYPE Function]\n# @BRIEF Retrieve logs for a specific task with optional filtering.\n# @PRE: task_id must exist.\n# @POST: Returns a list of log entries or raises 404.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [LogFilter]\n# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]\n# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.\n# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json\n# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.\n# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.\n# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.\n# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]\n@router.get(\"/{task_id}/logs\")\nasync def get_task_logs(\n task_id: str,\n level: str | None = Query(\n None, description=\"Filter by log level (DEBUG, INFO, WARNING, ERROR)\"\n ),\n source: str | None = Query(None, description=\"Filter by source component\"),\n search: str | None = Query(None, description=\"Text search in message\"),\n offset: int = Query(0, ge=0, description=\"Number of logs to skip\"),\n limit: int = Query(\n 100, ge=1, le=1000, description=\"Maximum number of logs to return\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_logs\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n\n log_filter = LogFilter(\n level=level.upper() if level else None,\n source=source,\n search=search,\n offset=offset,\n limit=limit,\n )\n return task_manager.get_task_logs(task_id, log_filter)\n\n\n# #endregion get_task_logs\n\n\n# #region get_task_log_stats [C:2] [TYPE Function]\n# @BRIEF Get statistics about logs for a task (counts by level and source).\n# @PRE: task_id must exist.\n# @POST: Returns log statistics or raises 404.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [LogStats]\n@router.get(\"/{task_id}/logs/stats\", response_model=LogStats)\nasync def get_task_log_stats(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_stats\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n stats_payload = task_manager.get_task_log_stats(task_id)\n if isinstance(stats_payload, LogStats):\n return stats_payload\n if isinstance(stats_payload, dict) and (\n \"total_count\" in stats_payload\n or \"by_level\" in stats_payload\n or \"by_source\" in stats_payload\n ):\n return LogStats(\n total_count=int(stats_payload.get(\"total_count\", 0) or 0),\n by_level=dict(stats_payload.get(\"by_level\") or {}),\n by_source=dict(stats_payload.get(\"by_source\") or {}),\n )\n flat_by_level = (\n dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}\n )\n return LogStats(\n total_count=sum(int(value or 0) for value in flat_by_level.values()),\n by_level={\n str(key): int(value or 0) for key, value in flat_by_level.items()\n },\n by_source={},\n )\n\n\n# #endregion get_task_log_stats\n\n\n# #region get_task_log_sources [C:2] [TYPE Function]\n# @BRIEF Get unique sources for a task's logs.\n# @PRE: task_id must exist.\n# @POST: Returns list of unique source names or raises 404.\n# @RELATION CALLS -> [TaskManager]\n@router.get(\"/{task_id}/logs/sources\", response_model=list[str])\nasync def get_task_log_sources(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_sources\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task_manager.get_task_log_sources(task_id)\n\n\n# #endregion get_task_log_sources\n\n\n# #region resolve_task [C:2] [TYPE Function]\n# @BRIEF Resolve a task that is awaiting mapping.\n# @PRE: task must be in AWAITING_MAPPING status.\n# @POST: Task is resolved and resumes execution.\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resolve\", response_model=Task)\nasync def resolve_task(\n task_id: str,\n request: ResolveTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resolve_task\"):\n try:\n await task_manager.resolve_task(task_id, request.resolution_params)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# #endregion resolve_task\n\n\n# #region resume_task [C:2] [TYPE Function]\n# @BRIEF Resume a task that is awaiting input (e.g., passwords).\n# @PRE: task must be in AWAITING_INPUT status.\n# @POST: Task resumes execution with provided input.\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resume\", response_model=Task)\nasync def resume_task(\n task_id: str,\n request: ResumeTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resume_task\"):\n try:\n task_manager.resume_task_with_password(task_id, request.passwords)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# #endregion resume_task\n\n\n# #region clear_tasks [C:2] [TYPE Function]\n# @BRIEF Clear tasks matching the status filter.\n# @PRE: task_manager is available.\n# @POST: Tasks are removed from memory/persistence.\n# @RELATION CALLS -> [TaskManager]\n@router.delete(\"\", status_code=status.HTTP_204_NO_CONTENT)\nasync def clear_tasks(\n status: TaskStatus | None = None,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"clear_tasks\", f\"status={status}\"):\n task_manager.clear_tasks(status)\n return\n\n\n# #endregion clear_tasks\n\n# #endregion TasksRouter\n" }, { "contract_id": "list_tasks", @@ -21288,318 +21153,6 @@ "tier_source": "AutoCalculated", "body": "# #region list_tasks [C:2] [TYPE Function]\n# @BRIEF Retrieve a list of tasks with pagination and optional status filter.\n# @PRE: task_manager must be available.\n# @POST: Returns a list of tasks.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]\n@router.get(\"\", response_model=list[Task])\nasync def list_tasks(\n limit: int = 10,\n offset: int = 0,\n status_filter: TaskStatus | None = Query(None, alias=\"status\"),\n task_type: str | None = Query(\n None, description=\"Task category: llm_validation, backup, migration\"\n ),\n plugin_id: list[str] | None = Query(\n None, description=\"Filter by plugin_id (repeatable query param)\"\n ),\n completed_only: bool = Query(\n False, description=\"Return only completed tasks (SUCCESS/FAILED)\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_tasks\"):\n plugin_filters = list(plugin_id) if plugin_id else []\n if task_type:\n if task_type not in TASK_TYPE_PLUGIN_MAP:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}\",\n )\n plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])\n\n return task_manager.get_tasks(\n limit=limit,\n offset=offset,\n status=status_filter,\n plugin_ids=plugin_filters or None,\n completed_only=completed_only,\n )\n\n\n# #endregion list_tasks\n" }, - { - "contract_id": "get_task", - "contract_type": "Function", - "file_path": "backend/src/api/routes/tasks.py", - "start_line": 175, - "end_line": 195, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "POST": "Returns task details or raises 404.", - "PRE": "task_id must exist.", - "PURPOSE": "Retrieve the details of a specific task." - }, - "relations": [ - { - "source_id": "get_task", - "relation_type": "CALLS", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region get_task [C:2] [TYPE Function]\n# @BRIEF Retrieve the details of a specific task.\n# @PRE: task_id must exist.\n# @POST: Returns task details or raises 404.\n# @RELATION CALLS -> [TaskManager]\n@router.get(\"/{task_id}\", response_model=Task)\nasync def get_task(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_task\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task\n\n\n# #endregion get_task\n" - }, - { - "contract_id": "get_task_logs", - "contract_type": "Function", - "file_path": "backend/src/api/routes/tasks.py", - "start_line": 198, - "end_line": 242, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "POST": "Returns a list of log entries or raises 404.", - "PRE": "task_id must exist.", - "PURPOSE": "Retrieve logs for a specific task with optional filtering.", - "TEST_CONTRACT": "TaskLogQueryInput -> List[LogEntry]", - "TEST_EDGE": "pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.", - "TEST_FIXTURE": "valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json", - "TEST_INVARIANT": "logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]", - "TEST_SCENARIO": "existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination." - }, - "relations": [ - { - "source_id": "get_task_logs", - "relation_type": "CALLS", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - }, - { - "source_id": "get_task_logs", - "relation_type": "DEPENDS_ON", - "target_id": "LogFilter", - "target_ref": "[LogFilter]" - } - ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is forbidden for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "INVARIANT", - "message": "@INVARIANT is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region get_task_logs [C:5] [TYPE Function]\n# @BRIEF Retrieve logs for a specific task with optional filtering.\n# @PRE: task_id must exist.\n# @POST: Returns a list of log entries or raises 404.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [LogFilter]\n# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]\n# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.\n# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json\n# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.\n# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.\n# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.\n# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]\n@router.get(\"/{task_id}/logs\")\nasync def get_task_logs(\n task_id: str,\n level: str | None = Query(\n None, description=\"Filter by log level (DEBUG, INFO, WARNING, ERROR)\"\n ),\n source: str | None = Query(None, description=\"Filter by source component\"),\n search: str | None = Query(None, description=\"Text search in message\"),\n offset: int = Query(0, ge=0, description=\"Number of logs to skip\"),\n limit: int = Query(\n 100, ge=1, le=1000, description=\"Maximum number of logs to return\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_logs\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n\n log_filter = LogFilter(\n level=level.upper() if level else None,\n source=source,\n search=search,\n offset=offset,\n limit=limit,\n )\n return task_manager.get_task_logs(task_id, log_filter)\n\n\n# #endregion get_task_logs\n" - }, - { - "contract_id": "get_task_log_stats", - "contract_type": "Function", - "file_path": "backend/src/api/routes/tasks.py", - "start_line": 245, - "end_line": 287, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "POST": "Returns log statistics or raises 404.", - "PRE": "task_id must exist.", - "PURPOSE": "Get statistics about logs for a task (counts by level and source)." - }, - "relations": [ - { - "source_id": "get_task_log_stats", - "relation_type": "CALLS", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - }, - { - "source_id": "get_task_log_stats", - "relation_type": "DEPENDS_ON", - "target_id": "LogStats", - "target_ref": "[LogStats]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region get_task_log_stats [C:2] [TYPE Function]\n# @BRIEF Get statistics about logs for a task (counts by level and source).\n# @PRE: task_id must exist.\n# @POST: Returns log statistics or raises 404.\n# @RELATION CALLS -> [TaskManager]\n# @RELATION DEPENDS_ON -> [LogStats]\n@router.get(\"/{task_id}/logs/stats\", response_model=LogStats)\nasync def get_task_log_stats(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_stats\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n stats_payload = task_manager.get_task_log_stats(task_id)\n if isinstance(stats_payload, LogStats):\n return stats_payload\n if isinstance(stats_payload, dict) and (\n \"total_count\" in stats_payload\n or \"by_level\" in stats_payload\n or \"by_source\" in stats_payload\n ):\n return LogStats(\n total_count=int(stats_payload.get(\"total_count\", 0) or 0),\n by_level=dict(stats_payload.get(\"by_level\") or {}),\n by_source=dict(stats_payload.get(\"by_source\") or {}),\n )\n flat_by_level = (\n dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}\n )\n return LogStats(\n total_count=sum(int(value or 0) for value in flat_by_level.values()),\n by_level={\n str(key): int(value or 0) for key, value in flat_by_level.items()\n },\n by_source={},\n )\n\n\n# #endregion get_task_log_stats\n" - }, - { - "contract_id": "get_task_log_sources", - "contract_type": "Function", - "file_path": "backend/src/api/routes/tasks.py", - "start_line": 290, - "end_line": 309, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "POST": "Returns list of unique source names or raises 404.", - "PRE": "task_id must exist.", - "PURPOSE": "Get unique sources for a task's logs." - }, - "relations": [ - { - "source_id": "get_task_log_sources", - "relation_type": "CALLS", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region get_task_log_sources [C:2] [TYPE Function]\n# @BRIEF Get unique sources for a task's logs.\n# @PRE: task_id must exist.\n# @POST: Returns list of unique source names or raises 404.\n# @RELATION CALLS -> [TaskManager]\n@router.get(\"/{task_id}/logs/sources\", response_model=list[str])\nasync def get_task_log_sources(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_sources\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task_manager.get_task_log_sources(task_id)\n\n\n# #endregion get_task_log_sources\n" - }, - { - "contract_id": "resolve_task", - "contract_type": "Function", - "file_path": "backend/src/api/routes/tasks.py", - "start_line": 312, - "end_line": 332, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "POST": "Task is resolved and resumes execution.", - "PRE": "task must be in AWAITING_MAPPING status.", - "PURPOSE": "Resolve a task that is awaiting mapping." - }, - "relations": [ - { - "source_id": "resolve_task", - "relation_type": "CALLS", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region resolve_task [C:2] [TYPE Function]\n# @BRIEF Resolve a task that is awaiting mapping.\n# @PRE: task must be in AWAITING_MAPPING status.\n# @POST: Task is resolved and resumes execution.\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resolve\", response_model=Task)\nasync def resolve_task(\n task_id: str,\n request: ResolveTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resolve_task\"):\n try:\n await task_manager.resolve_task(task_id, request.resolution_params)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# #endregion resolve_task\n" - }, { "contract_id": "resume_task", "contract_type": "Function", @@ -23882,17 +23435,17 @@ "contract_type": "Module", "file_path": "backend/src/app.py", "start_line": 1, - "end_line": 435, + "end_line": 436, "tier": "TIER_3", "complexity": 5, "metadata": { "COMPLEXITY": 5, "DATA_CONTRACT": "[HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]", "INVARIANT": "All WebSocket connections must be properly cleaned up on disconnect.", - "LAYER": "UI (API)", + "LAYER": "API", "POST": "FastAPI app instance is created, middleware configured, and routes registered.", "PRE": "Python environment and dependencies installed; configuration database available.", - "PURPOSE": "The main entry point for the FastAPI application. It initializes the app, configures CORS, sets up dependencies, includes API routers, and defines the WebSocket endpoint for log streaming.", + "PURPOSE": "The main entry point for the FastAPI application.", "SIDE_EFFECT": "Starts background scheduler and binds network ports for HTTP/WS traffic." }, "relations": [ @@ -23909,28 +23462,10 @@ "target_ref": "[ApiRoutesModule]" } ], - "schema_warnings": [ - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI (API)' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI (API)" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AppModule [C:5] [TYPE Module] [SEMANTICS fastapi, websocket, startup, scheduler, middleware]\n# @BRIEF The main entry point for the FastAPI application. It initializes the app, configures CORS, sets up dependencies, includes API routers, and defines the WebSocket endpoint for log streaming.\n# @LAYER UI (API)\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect.\n# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect.\n# @PRE Python environment and dependencies installed; configuration database available.\n# @POST FastAPI app instance is created, middleware configured, and routes registered.\n# @SIDE_EFFECT Starts background scheduler and binds network ports for HTTP/WS traffic.\n# @DATA_CONTRACT [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]\n\nimport os\nfrom pathlib import Path\n\n# project_root is used for static files mounting\nproject_root = Path(__file__).resolve().parent.parent.parent\nimport asyncio\n\nfrom fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import FileResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom starlette.middleware.sessions import SessionMiddleware\n\nfrom .api import auth\nfrom .api.routes import (\n admin,\n assistant,\n clean_release,\n clean_release_v2,\n connections,\n dashboards,\n dataset_review,\n datasets,\n environments,\n git,\n health,\n llm,\n mappings,\n migration,\n plugins,\n profile,\n reports,\n settings,\n storage,\n tasks,\n translate,\n)\nfrom .core.auth.security import get_password_hash\nfrom .core.cot_logger import seed_trace_id\nfrom .core.database import AuthSessionLocal\nfrom .core.encryption_key import ensure_encryption_key\nfrom .core.logger import belief_scope, logger\nfrom .core.utils.network import NetworkError\nfrom .dependencies import get_scheduler_service, get_task_manager\nfrom .models.auth import Role, User\n\n# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n)\n# #endregion FastAPI_App\n# #region ensure_initial_admin_user [C:3] [TYPE Function]\n# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.\n# @RELATION DEPENDS_ON -> AuthRepository\ndef ensure_initial_admin_user() -> None:\n raw_flag = os.getenv(\"INITIAL_ADMIN_CREATE\", \"false\").strip().lower()\n if raw_flag not in {\"1\", \"true\", \"yes\", \"on\"}:\n return\n username = os.getenv(\"INITIAL_ADMIN_USERNAME\", \"\").strip()\n password = os.getenv(\"INITIAL_ADMIN_PASSWORD\", \"\").strip()\n if not username or not password:\n logger.warning(\n \"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap.\"\n )\n return\n db = AuthSessionLocal()\n try:\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n admin_role = Role(name=\"Admin\", description=\"System Administrator\")\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.info(\n \"Initial admin bootstrap skipped: user '%s' already exists.\", username\n )\n return\n new_user = User(\n username=username,\n email=None,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(\n \"Initial admin user '%s' created from environment bootstrap.\", username\n )\n except Exception as exc:\n db.rollback()\n logger.error(\"Failed to bootstrap initial admin user: %s\", exc)\n raise\n finally:\n db.close()\n# #endregion ensure_initial_admin_user\n# #region startup_event [C:3] [TYPE Function]\n# @BRIEF Handles application startup tasks, such as starting the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is started.\n# Startup event\n@app.on_event(\"startup\")\nasync def startup_event():\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n ensure_encryption_key()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n scheduler.start()\n# #endregion startup_event\n# #region shutdown_event [C:3] [TYPE Function]\n# @BRIEF Handles application shutdown tasks, such as stopping the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is stopped.\n# Shutdown event\n@app.on_event(\"shutdown\")\nasync def shutdown_event():\n seed_trace_id()\n with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n# #endregion shutdown_event\n# #region app_middleware [TYPE Block]\n# @BRIEF Configure application-wide middleware (Session, CORS).\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\napp.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)\n# Configure CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Adjust this in production\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n# #endregion app_middleware\n# #region network_error_handler [C:1] [TYPE Function]\n# @BRIEF Global exception handler for NetworkError.\n# @PRE request is a FastAPI Request object.\n# @POST Returns 503 HTTP Exception.\n@app.exception_handler(NetworkError)\nasync def network_error_handler(request: Request, exc: NetworkError):\n with belief_scope(\"network_error_handler\"):\n logger.error(f\"Network error: {exc}\")\n return HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion network_error_handler\n# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE request is a FastAPI Request object.\n# @POST Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n try:\n response = await call_next(request)\n if not is_polling:\n logger.info(\n f\"Response status: {response.status_code} for {request.url.path}\"\n )\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n# Seed trace_id on every request — MUST be the outermost middleware so trace_id\n# is seeded before log_requests (or any other middleware) runs.\nfrom .core.middleware.trace import TraceContextMiddleware\n\napp.add_middleware(TraceContextMiddleware)\n# #region API_Routes [C:3] [TYPE Block]\n# @BRIEF Register all FastAPI route groups exposed by the application entrypoint.\n# @RELATION DEPENDS_ON -> [FastAPI_App]\n# @RELATION DEPENDS_ON -> [Route_Group_Contracts]\n# @RELATION DEPENDS_ON -> [AuthApi]\n# @RELATION DEPENDS_ON -> [AdminApi]\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ConnectionsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]\n# Include API routes\napp.include_router(auth.router)\napp.include_router(admin.router)\napp.include_router(plugins.router, prefix=\"/api/plugins\", tags=[\"Plugins\"])\napp.include_router(tasks.router, prefix=\"/api/tasks\", tags=[\"Tasks\"])\napp.include_router(settings.router, prefix=\"/api/settings\", tags=[\"Settings\"])\napp.include_router(\n connections.router, prefix=\"/api/settings/connections\", tags=[\"Connections\"]\n)\napp.include_router(environments.router, tags=[\"Environments\"])\napp.include_router(mappings.router, prefix=\"/api/mappings\", tags=[\"Mappings\"])\napp.include_router(migration.router)\napp.include_router(git.router, prefix=\"/api/git\", tags=[\"Git\"])\napp.include_router(llm.router, prefix=\"/api/llm\", tags=[\"LLM\"])\napp.include_router(storage.router, prefix=\"/api/storage\", tags=[\"Storage\"])\napp.include_router(dashboards.router)\napp.include_router(datasets.router)\napp.include_router(reports.router)\napp.include_router(assistant.router, prefix=\"/api/assistant\", tags=[\"Assistant\"])\napp.include_router(clean_release.router)\napp.include_router(clean_release_v2.router)\napp.include_router(profile.router)\napp.include_router(dataset_review.router)\napp.include_router(health.router)\napp.include_router(translate.router)\n# #endregion API_Routes\n# #region api.include_routers [C:1] [TYPE Action] [SEMANTICS routes, registration, api]\n# @BRIEF Registers all API routers with the FastAPI application.\n# @LAYER API\n# #endregion api.include_routers\n# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE task_id must be a valid task ID.\n# @POST WebSocket connection is managed and logs are streamed until disconnect.\n# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network.\n# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects]\n# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.\n# @UX_STATE Connecting -> Streaming -> (Disconnected)\n#\n# @TEST_CONTRACT WebSocketLogStreamApi ->\n# {\n# required_fields: {websocket: WebSocket, task_id: str},\n# optional_fields: {source: str, level: str},\n# invariants: [\n# \"Accepts the WebSocket connection\",\n# \"Applies source and level filters correctly to streamed logs\",\n# \"Cleans up subscriptions on disconnect\"\n# ]\n# }\n# @TEST_FIXTURE valid_ws_connection -> {\"task_id\": \"test_1\", \"source\": \"plugin\"}\n# @TEST_EDGE task_not_found_ws -> closes connection or sends error\n# @TEST_EDGE empty_task_logs -> waits for new logs\n# @TEST_INVARIANT consistent_streaming -> verifies: [valid_ws_connection]\n@app.websocket(\"/ws/logs/{task_id}\")\nasync def websocket_endpoint(\n websocket: WebSocket, task_id: str, source: str = None, level: str = None\n):\n \"\"\"\n WebSocket endpoint for real-time log streaming with optional server-side filtering.\n Query Parameters:\n source: Filter logs by source component (e.g., \"plugin\", \"superset_api\")\n level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)\n \"\"\"\n seed_trace_id()\n with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n await websocket.accept()\n source_filter = source.lower() if source else None\n level_filter = level.upper() if level else None\n level_hierarchy = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n min_level = level_hierarchy.get(level_filter, 0) if level_filter else 0\n logger.reason(\n \"Accepted WebSocket log stream connection\",\n extra={\n \"task_id\": task_id,\n \"source_filter\": source_filter,\n \"level_filter\": level_filter,\n \"min_level\": min_level,\n },\n )\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_logs(task_id)\n logger.reason(\n \"Subscribed WebSocket client to task log queue\",\n extra={\"task_id\": task_id},\n )\n def matches_filters(log_entry) -> bool:\n \"\"\"Check if log entry matches the filter criteria.\"\"\"\n log_source = getattr(log_entry, \"source\", None)\n if source_filter and str(log_source or \"\").lower() != source_filter:\n return False\n if level_filter:\n log_level = level_hierarchy.get(str(log_entry.level).upper(), 0)\n if log_level < min_level:\n return False\n return True\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\n initial_logs = task_manager.get_task_logs(task_id)\n initial_sent = 0\n for log_entry in initial_logs:\n if matches_filters(log_entry):\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n initial_sent += 1\n logger.reflect(\n \"Initial task log replay completed\",\n extra={\n \"task_id\": task_id,\n \"replayed_logs\": initial_sent,\n \"total_available_logs\": len(initial_logs),\n },\n )\n task = task_manager.get_task(task_id)\n if task and task.status == \"AWAITING_INPUT\" and task.input_request:\n synthetic_log = {\n \"timestamp\": task.logs[-1].timestamp.isoformat()\n if task.logs\n else \"2024-01-01T00:00:00\",\n \"level\": \"INFO\",\n \"message\": \"Task paused for user input (Connection Re-established)\",\n \"context\": {\"input_request\": task.input_request},\n }\n await websocket.send_json(synthetic_log)\n logger.reason(\n \"Replayed awaiting-input prompt to restored WebSocket client\",\n extra={\"task_id\": task_id, \"task_status\": task.status},\n )\n while True:\n log_entry = await queue.get()\n if not matches_filters(log_entry):\n continue\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n logger.reflect(\n \"Forwarded task log entry to WebSocket client\",\n extra={\n \"task_id\": task_id,\n \"level\": log_dict.get(\"level\"),\n },\n )\n if (\n \"Task completed successfully\" in log_entry.message\n or \"Task failed\" in log_entry.message\n ):\n logger.reason(\n \"Observed terminal task log entry; delaying to preserve client visibility\",\n extra={\"task_id\": task_id, \"message\": log_entry.message},\n )\n await asyncio.sleep(2)\n except WebSocketDisconnect:\n logger.reason(\n \"WebSocket client disconnected from task log stream\",\n extra={\"task_id\": task_id},\n )\n except Exception as exc:\n logger.explore(\n \"WebSocket log streaming encountered an unexpected failure\",\n extra={\"task_id\": task_id, \"error\": str(exc)},\n )\n raise\n finally:\n task_manager.unsubscribe_logs(task_id, queue)\n logger.reflect(\n \"Released WebSocket log queue subscription\",\n extra={\"task_id\": task_id},\n )\n# #endregion websocket_endpoint\n# #region StaticFiles [C:1] [TYPE Mount] [SEMANTICS static, frontend, spa]\n# @BRIEF Mounts the frontend build directory to serve static assets.\nfrontend_path = project_root / \"frontend\" / \"build\"\nif frontend_path.exists():\n app.mount(\n \"/_app\", StaticFiles(directory=str(frontend_path / \"_app\")), name=\"static\"\n )\n # #region serve_spa [TYPE Function] [C:1]\n # @PURPOSE Serves the SPA frontend for any path not matched by API routes.\n # @PRE frontend_path exists.\n # @POST Returns the requested file or index.html.\n @app.get(\"/{file_path:path}\", include_in_schema=False)\n async def serve_spa(file_path: str):\n with belief_scope(\"serve_spa\"):\n # Only serve SPA for non-API paths\n # API routes are registered separately and should be matched by FastAPI first\n if file_path and (\n file_path.startswith(\"api/\")\n or file_path.startswith(\"/api/\")\n or file_path == \"api\"\n ):\n # This should not happen if API routers are properly registered\n # Return 404 instead of serving HTML\n raise HTTPException(\n status_code=404, detail=f\"API endpoint not found: {file_path}\"\n )\n full_path = frontend_path / file_path\n if file_path and full_path.is_file():\n return FileResponse(str(full_path))\n return FileResponse(str(frontend_path / \"index.html\"))\n # #endregion serve_spa\nelse:\n # #region read_root [TYPE Function] [C:1]\n # @PURPOSE A simple root endpoint to confirm that the API is running when frontend is missing.\n # @PRE None.\n # @POST Returns a JSON message indicating API status.\n @app.get(\"/\")\n async def read_root():\n with belief_scope(\"read_root\"):\n return {\n \"message\": \"Superset Tools API is running (Frontend build not found)\"\n }\n # #endregion read_root\n# #endregion StaticFiles\n# #endregion AppModule\n" + "body": "# #region AppModule [C:5] [TYPE Module] [SEMANTICS fastapi, websocket, startup, scheduler, middleware]\n# @BRIEF The main entry point for the FastAPI application.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect.\n# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect.\n# @PRE Python environment and dependencies installed; configuration database available.\n# @POST FastAPI app instance is created, middleware configured, and routes registered.\n# @SIDE_EFFECT Starts background scheduler and binds network ports for HTTP/WS traffic.\n# @DATA_CONTRACT [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]\n\nimport os\nfrom pathlib import Path\n\n# project_root is used for static files mounting\nproject_root = Path(__file__).resolve().parent.parent.parent\nimport asyncio\n\nfrom fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import FileResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom starlette.middleware.sessions import SessionMiddleware\n\nfrom .api import auth\nfrom .api.routes import (\n admin,\n assistant,\n clean_release,\n clean_release_v2,\n connections,\n dashboards,\n dataset_review,\n datasets,\n environments,\n git,\n health,\n llm,\n mappings,\n migration,\n plugins,\n profile,\n reports,\n settings,\n storage,\n tasks,\n translate,\n)\nfrom .core.auth.security import get_password_hash\nfrom .core.cot_logger import seed_trace_id\nfrom .core.database import AuthSessionLocal, init_db\nfrom .core.encryption_key import ensure_encryption_key\nfrom .core.logger import belief_scope, logger\nfrom .core.utils.network import NetworkError\nfrom .dependencies import get_scheduler_service, get_task_manager\nfrom .models.auth import Role, User\n\n# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n)\n# #endregion FastAPI_App\n# #region ensure_initial_admin_user [C:3] [TYPE Function]\n# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.\n# @RELATION DEPENDS_ON -> AuthRepository\ndef ensure_initial_admin_user() -> None:\n raw_flag = os.getenv(\"INITIAL_ADMIN_CREATE\", \"false\").strip().lower()\n if raw_flag not in {\"1\", \"true\", \"yes\", \"on\"}:\n return\n username = os.getenv(\"INITIAL_ADMIN_USERNAME\", \"\").strip()\n password = os.getenv(\"INITIAL_ADMIN_PASSWORD\", \"\").strip()\n if not username or not password:\n logger.warning(\n \"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap.\"\n )\n return\n db = AuthSessionLocal()\n try:\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n admin_role = Role(name=\"Admin\", description=\"System Administrator\")\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.info(\n \"Initial admin bootstrap skipped: user '%s' already exists.\", username\n )\n return\n new_user = User(\n username=username,\n email=None,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(\n \"Initial admin user '%s' created from environment bootstrap.\", username\n )\n except Exception as exc:\n db.rollback()\n logger.error(\"Failed to bootstrap initial admin user: %s\", exc)\n raise\n finally:\n db.close()\n# #endregion ensure_initial_admin_user\n# #region startup_event [C:3] [TYPE Function]\n# @BRIEF Handles application startup tasks, such as starting the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is started.\n# Startup event\n@app.on_event(\"startup\")\nasync def startup_event():\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n ensure_encryption_key()\n init_db()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n scheduler.start()\n# #endregion startup_event\n# #region shutdown_event [C:3] [TYPE Function]\n# @BRIEF Handles application shutdown tasks, such as stopping the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is stopped.\n# Shutdown event\n@app.on_event(\"shutdown\")\nasync def shutdown_event():\n seed_trace_id()\n with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n# #endregion shutdown_event\n# #region app_middleware [TYPE Block]\n# @BRIEF Configure application-wide middleware (Session, CORS).\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\napp.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)\n# Configure CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Adjust this in production\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n# #endregion app_middleware\n# #region network_error_handler [C:1] [TYPE Function]\n# @BRIEF Global exception handler for NetworkError.\n# @PRE request is a FastAPI Request object.\n# @POST Returns 503 HTTP Exception.\n@app.exception_handler(NetworkError)\nasync def network_error_handler(request: Request, exc: NetworkError):\n with belief_scope(\"network_error_handler\"):\n logger.error(f\"Network error: {exc}\")\n return HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion network_error_handler\n# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE request is a FastAPI Request object.\n# @POST Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n try:\n response = await call_next(request)\n if not is_polling:\n logger.info(\n f\"Response status: {response.status_code} for {request.url.path}\"\n )\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n# Seed trace_id on every request — MUST be the outermost middleware so trace_id\n# is seeded before log_requests (or any other middleware) runs.\nfrom .core.middleware.trace import TraceContextMiddleware\n\napp.add_middleware(TraceContextMiddleware)\n# #region API_Routes [C:3] [TYPE Block]\n# @BRIEF Register all FastAPI route groups exposed by the application entrypoint.\n# @RELATION DEPENDS_ON -> [FastAPI_App]\n# @RELATION DEPENDS_ON -> [Route_Group_Contracts]\n# @RELATION DEPENDS_ON -> [AuthApi]\n# @RELATION DEPENDS_ON -> [AdminApi]\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ConnectionsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]\n# Include API routes\napp.include_router(auth.router)\napp.include_router(admin.router)\napp.include_router(plugins.router, prefix=\"/api/plugins\", tags=[\"Plugins\"])\napp.include_router(tasks.router, prefix=\"/api/tasks\", tags=[\"Tasks\"])\napp.include_router(settings.router, prefix=\"/api/settings\", tags=[\"Settings\"])\napp.include_router(\n connections.router, prefix=\"/api/settings/connections\", tags=[\"Connections\"]\n)\napp.include_router(environments.router, tags=[\"Environments\"])\napp.include_router(mappings.router, prefix=\"/api/mappings\", tags=[\"Mappings\"])\napp.include_router(migration.router)\napp.include_router(git.router, prefix=\"/api/git\", tags=[\"Git\"])\napp.include_router(llm.router, prefix=\"/api/llm\", tags=[\"LLM\"])\napp.include_router(storage.router, prefix=\"/api/storage\", tags=[\"Storage\"])\napp.include_router(dashboards.router)\napp.include_router(datasets.router)\napp.include_router(reports.router)\napp.include_router(assistant.router, prefix=\"/api/assistant\", tags=[\"Assistant\"])\napp.include_router(clean_release.router)\napp.include_router(clean_release_v2.router)\napp.include_router(profile.router)\napp.include_router(dataset_review.router)\napp.include_router(health.router)\napp.include_router(translate.router)\n# #endregion API_Routes\n# #region api.include_routers [C:1] [TYPE Action] [SEMANTICS routes, registration, api]\n# @BRIEF Registers all API routers with the FastAPI application.\n# @LAYER API\n# #endregion api.include_routers\n# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE task_id must be a valid task ID.\n# @POST WebSocket connection is managed and logs are streamed until disconnect.\n# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network.\n# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects]\n# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.\n# @UX_STATE Connecting -> Streaming -> (Disconnected)\n#\n# @TEST_CONTRACT WebSocketLogStreamApi ->\n# {\n# required_fields: {websocket: WebSocket, task_id: str},\n# optional_fields: {source: str, level: str},\n# invariants: [\n# \"Accepts the WebSocket connection\",\n# \"Applies source and level filters correctly to streamed logs\",\n# \"Cleans up subscriptions on disconnect\"\n# ]\n# }\n# @TEST_FIXTURE valid_ws_connection -> {\"task_id\": \"test_1\", \"source\": \"plugin\"}\n# @TEST_EDGE task_not_found_ws -> closes connection or sends error\n# @TEST_EDGE empty_task_logs -> waits for new logs\n# @TEST_INVARIANT consistent_streaming -> verifies: [valid_ws_connection]\n@app.websocket(\"/ws/logs/{task_id}\")\nasync def websocket_endpoint(\n websocket: WebSocket, task_id: str, source: str = None, level: str = None\n):\n \"\"\"\n WebSocket endpoint for real-time log streaming with optional server-side filtering.\n Query Parameters:\n source: Filter logs by source component (e.g., \"plugin\", \"superset_api\")\n level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)\n \"\"\"\n seed_trace_id()\n with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n await websocket.accept()\n source_filter = source.lower() if source else None\n level_filter = level.upper() if level else None\n level_hierarchy = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n min_level = level_hierarchy.get(level_filter, 0) if level_filter else 0\n logger.reason(\n \"Accepted WebSocket log stream connection\",\n extra={\n \"task_id\": task_id,\n \"source_filter\": source_filter,\n \"level_filter\": level_filter,\n \"min_level\": min_level,\n },\n )\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_logs(task_id)\n logger.reason(\n \"Subscribed WebSocket client to task log queue\",\n extra={\"task_id\": task_id},\n )\n def matches_filters(log_entry) -> bool:\n \"\"\"Check if log entry matches the filter criteria.\"\"\"\n log_source = getattr(log_entry, \"source\", None)\n if source_filter and str(log_source or \"\").lower() != source_filter:\n return False\n if level_filter:\n log_level = level_hierarchy.get(str(log_entry.level).upper(), 0)\n if log_level < min_level:\n return False\n return True\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\n initial_logs = task_manager.get_task_logs(task_id)\n initial_sent = 0\n for log_entry in initial_logs:\n if matches_filters(log_entry):\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n initial_sent += 1\n logger.reflect(\n \"Initial task log replay completed\",\n extra={\n \"task_id\": task_id,\n \"replayed_logs\": initial_sent,\n \"total_available_logs\": len(initial_logs),\n },\n )\n task = task_manager.get_task(task_id)\n if task and task.status == \"AWAITING_INPUT\" and task.input_request:\n synthetic_log = {\n \"timestamp\": task.logs[-1].timestamp.isoformat()\n if task.logs\n else \"2024-01-01T00:00:00\",\n \"level\": \"INFO\",\n \"message\": \"Task paused for user input (Connection Re-established)\",\n \"context\": {\"input_request\": task.input_request},\n }\n await websocket.send_json(synthetic_log)\n logger.reason(\n \"Replayed awaiting-input prompt to restored WebSocket client\",\n extra={\"task_id\": task_id, \"task_status\": task.status},\n )\n while True:\n log_entry = await queue.get()\n if not matches_filters(log_entry):\n continue\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n logger.reflect(\n \"Forwarded task log entry to WebSocket client\",\n extra={\n \"task_id\": task_id,\n \"level\": log_dict.get(\"level\"),\n },\n )\n if (\n \"Task completed successfully\" in log_entry.message\n or \"Task failed\" in log_entry.message\n ):\n logger.reason(\n \"Observed terminal task log entry; delaying to preserve client visibility\",\n extra={\"task_id\": task_id, \"message\": log_entry.message},\n )\n await asyncio.sleep(2)\n except WebSocketDisconnect:\n logger.reason(\n \"WebSocket client disconnected from task log stream\",\n extra={\"task_id\": task_id},\n )\n except Exception as exc:\n logger.explore(\n \"WebSocket log streaming encountered an unexpected failure\",\n extra={\"task_id\": task_id, \"error\": str(exc)},\n )\n raise\n finally:\n task_manager.unsubscribe_logs(task_id, queue)\n logger.reflect(\n \"Released WebSocket log queue subscription\",\n extra={\"task_id\": task_id},\n )\n# #endregion websocket_endpoint\n# #region StaticFiles [C:1] [TYPE Mount] [SEMANTICS static, frontend, spa]\n# @BRIEF Mounts the frontend build directory to serve static assets.\nfrontend_path = project_root / \"frontend\" / \"build\"\nif frontend_path.exists():\n app.mount(\n \"/_app\", StaticFiles(directory=str(frontend_path / \"_app\")), name=\"static\"\n )\n # #region serve_spa [TYPE Function] [C:1]\n # @PURPOSE Serves the SPA frontend for any path not matched by API routes.\n # @PRE frontend_path exists.\n # @POST Returns the requested file or index.html.\n @app.get(\"/{file_path:path}\", include_in_schema=False)\n async def serve_spa(file_path: str):\n with belief_scope(\"serve_spa\"):\n # Only serve SPA for non-API paths\n # API routes are registered separately and should be matched by FastAPI first\n if file_path and (\n file_path.startswith(\"api/\")\n or file_path.startswith(\"/api/\")\n or file_path == \"api\"\n ):\n # This should not happen if API routers are properly registered\n # Return 404 instead of serving HTML\n raise HTTPException(\n status_code=404, detail=f\"API endpoint not found: {file_path}\"\n )\n full_path = frontend_path / file_path\n if file_path and full_path.is_file():\n return FileResponse(str(full_path))\n return FileResponse(str(frontend_path / \"index.html\"))\n # #endregion serve_spa\nelse:\n # #region read_root [TYPE Function] [C:1]\n # @PURPOSE A simple root endpoint to confirm that the API is running when frontend is missing.\n # @PRE None.\n # @POST Returns a JSON message indicating API status.\n @app.get(\"/\")\n async def read_root():\n with belief_scope(\"read_root\"):\n return {\n \"message\": \"Superset Tools API is running (Frontend build not found)\"\n }\n # #endregion read_root\n# #endregion StaticFiles\n# #endregion AppModule\n" }, { "contract_id": "FastAPI_App", @@ -24056,7 +23591,7 @@ "contract_type": "Function", "file_path": "backend/src/app.py", "start_line": 117, - "end_line": 131, + "end_line": 132, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -24095,14 +23630,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region startup_event [C:3] [TYPE Function]\n# @BRIEF Handles application startup tasks, such as starting the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is started.\n# Startup event\n@app.on_event(\"startup\")\nasync def startup_event():\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n ensure_encryption_key()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n scheduler.start()\n# #endregion startup_event\n" + "body": "# #region startup_event [C:3] [TYPE Function]\n# @BRIEF Handles application startup tasks, such as starting the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is started.\n# Startup event\n@app.on_event(\"startup\")\nasync def startup_event():\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n ensure_encryption_key()\n init_db()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n scheduler.start()\n# #endregion startup_event\n" }, { "contract_id": "shutdown_event", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 132, - "end_line": 144, + "start_line": 133, + "end_line": 145, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -24147,8 +23682,8 @@ "contract_id": "app_middleware", "contract_type": "Block", "file_path": "backend/src/app.py", - "start_line": 145, - "end_line": 159, + "start_line": 146, + "end_line": 160, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -24164,8 +23699,8 @@ "contract_id": "network_error_handler", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 160, - "end_line": 172, + "start_line": 161, + "end_line": 173, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -24203,8 +23738,8 @@ "contract_id": "log_requests", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 173, - "end_line": 198, + "start_line": 174, + "end_line": 199, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -24249,8 +23784,8 @@ "contract_id": "API_Routes", "contract_type": "Block", "file_path": "backend/src/app.py", - "start_line": 204, - "end_line": 242, + "start_line": 205, + "end_line": 243, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -24334,8 +23869,8 @@ "contract_id": "api.include_routers", "contract_type": "Action", "file_path": "backend/src/app.py", - "start_line": 243, - "end_line": 246, + "start_line": 244, + "end_line": 247, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -24429,8 +23964,8 @@ "contract_id": "websocket_endpoint", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 247, - "end_line": 390, + "start_line": 248, + "end_line": 391, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -24488,8 +24023,8 @@ "contract_id": "StaticFiles", "contract_type": "Mount", "file_path": "backend/src/app.py", - "start_line": 391, - "end_line": 434, + "start_line": 392, + "end_line": 435, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -26938,14 +26473,18 @@ "contract_type": "Module", "file_path": "backend/src/core/auth/jwt.py", "start_line": 1, - "end_line": 60, - "tier": "TIER_2", - "complexity": 3, + "end_line": 64, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "TokenPayload -> JWT string", "INVARIANT": "Tokens must include expiration time and user identifier.", "LAYER": "Core", - "PURPOSE": "JWT token generation and validation logic." + "POST": "Token encode/decode functions exported", + "PRE": "JWT secret configured in environment", + "PURPOSE": "JWT token generation and validation logic.", + "SIDE_EFFECT": "None" }, "relations": [ { @@ -26962,15 +26501,6 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -26992,14 +26522,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthJwtModule [C:3] [TYPE Module] [SEMANTICS auth, validate, jwt, token]\n#\n# @BRIEF JWT token generation and validation logic.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [auth_config]\n# @RELATION USES -> [auth_config]\n#\n# @INVARIANT: Tokens must include expiration time and user identifier.\n\nfrom datetime import datetime, timedelta\n\nfrom jose import jwt\n\nfrom ..logger import belief_scope\nfrom .config import auth_config\n\n\n# #region create_access_token [TYPE Function]\n# @BRIEF Generates a new JWT access token.\n# @PRE: data dict contains 'sub' (user_id) and optional 'scopes' (roles).\n# @POST: Returns a signed JWT string.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:\n with belief_scope(\"create_access_token\"):\n to_encode = data.copy()\n if expires_delta:\n expire = datetime.utcnow() + expires_delta\n else:\n expire = datetime.utcnow() + timedelta(\n minutes=auth_config.ACCESS_TOKEN_EXPIRE_MINUTES\n )\n\n to_encode.update({\"exp\": expire})\n encoded_jwt = jwt.encode(\n to_encode, auth_config.SECRET_KEY, algorithm=auth_config.ALGORITHM\n )\n return encoded_jwt\n\n\n# #endregion create_access_token\n\n\n# #region decode_token [TYPE Function]\n# @BRIEF Decodes and validates a JWT token.\n# @PRE: token is a signed JWT string.\n# @POST: Returns the decoded payload if valid.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef decode_token(token: str) -> dict:\n with belief_scope(\"decode_token\"):\n payload = jwt.decode(\n token, auth_config.SECRET_KEY, algorithms=[auth_config.ALGORITHM]\n )\n return payload\n\n\n# #endregion decode_token\n\n# #endregion AuthJwtModule\n" + "body": "# #region AuthJwtModule [C:5] [TYPE Module] [SEMANTICS auth, validate, jwt, token]\n#\n# @BRIEF JWT token generation and validation logic.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [auth_config]\n# @RELATION USES -> [auth_config]\n#\n# @INVARIANT: Tokens must include expiration time and user identifier.\n# @PRE: JWT secret configured in environment\n# @POST: Token encode/decode functions exported\n# @SIDE_EFFECT: None\n# @DATA_CONTRACT: TokenPayload -> JWT string\n\nfrom datetime import datetime, timedelta\n\nfrom jose import jwt\n\nfrom ..logger import belief_scope\nfrom .config import auth_config\n\n\n# #region create_access_token [TYPE Function]\n# @BRIEF Generates a new JWT access token.\n# @PRE: data dict contains 'sub' (user_id) and optional 'scopes' (roles).\n# @POST: Returns a signed JWT string.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:\n with belief_scope(\"create_access_token\"):\n to_encode = data.copy()\n if expires_delta:\n expire = datetime.utcnow() + expires_delta\n else:\n expire = datetime.utcnow() + timedelta(\n minutes=auth_config.ACCESS_TOKEN_EXPIRE_MINUTES\n )\n\n to_encode.update({\"exp\": expire})\n encoded_jwt = jwt.encode(\n to_encode, auth_config.SECRET_KEY, algorithm=auth_config.ALGORITHM\n )\n return encoded_jwt\n\n\n# #endregion create_access_token\n\n\n# #region decode_token [TYPE Function]\n# @BRIEF Decodes and validates a JWT token.\n# @PRE: token is a signed JWT string.\n# @POST: Returns the decoded payload if valid.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef decode_token(token: str) -> dict:\n with belief_scope(\"decode_token\"):\n payload = jwt.decode(\n token, auth_config.SECRET_KEY, algorithms=[auth_config.ALGORITHM]\n )\n return payload\n\n\n# #endregion decode_token\n\n# #endregion AuthJwtModule\n" }, { "contract_id": "create_access_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 18, - "end_line": 41, + "start_line": 22, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -27052,8 +26582,8 @@ "contract_id": "decode_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 44, - "end_line": 58, + "start_line": 48, + "end_line": 62, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -27107,62 +26637,38 @@ "contract_type": "Module", "file_path": "backend/src/core/auth/logger.py", "start_line": 1, - "end_line": 28, - "tier": "TIER_2", - "complexity": 3, + "end_line": 34, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "AuthEvent -> LogEntry", "INVARIANT": "Must not log sensitive data like passwords or full tokens.", "LAYER": "Core", - "PURPOSE": "Audit logging for security-related events." + "POST": "Audit logging functions exported", + "PRE": "Auth module initialized", + "PURPOSE": "Structured auth logging module for audit trail generation.", + "SIDE_EFFECT": "Writes auth audit log entries" }, "relations": [ { "source_id": "AuthLoggerModule", - "relation_type": "USES", - "target_id": "belief_scope", - "target_ref": "belief_scope" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate USES is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "USES" - } + "relation_type": "DEPENDS_ON", + "target_id": "core_logger", + "target_ref": "[core_logger]" } ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthLoggerModule [C:3] [TYPE Module] [SEMANTICS auth, audit, logging]\n#\n# @BRIEF Audit logging for security-related events.\n# @LAYER: Core\n# @RELATION USES -> belief_scope\n#\n# @INVARIANT: Must not log sensitive data like passwords or full tokens.\n\nfrom datetime import datetime\n\nfrom ..logger import belief_scope, logger\n\n\n# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE: event_type and username are strings.\n# @POST: Security event is written to the application log.\n# @RELATION USES -> logger\ndef log_security_event(event_type: str, username: str, details: dict = None):\n with belief_scope(\"log_security_event\", f\"{event_type}:{username}\"):\n timestamp = datetime.utcnow().isoformat()\n msg = f\"[AUDIT][{timestamp}][{event_type}] User: {username}\"\n if details:\n msg += f\" Details: {details}\"\n logger.info(msg)\n# #endregion log_security_event\n\n# #endregion AuthLoggerModule\n" + "body": "# #region AuthLoggerModule [C:5] [TYPE Module] [SEMANTICS auth, audit, logging]\n#\n# @BRIEF Structured auth logging module for audit trail generation.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [core_logger]\n#\n# @INVARIANT: Must not log sensitive data like passwords or full tokens.\n# @PRE: Auth module initialized\n# @POST: Audit logging functions exported\n# @SIDE_EFFECT: Writes auth audit log entries\n# @DATA_CONTRACT: AuthEvent -> LogEntry\n# @SIDE_EFFECT: Writes auth audit log entries\n# @DATA_CONTRACT: AuthEvent -> LogEntry\n\nfrom datetime import datetime\n\nfrom ..logger import belief_scope, logger\n\n\n# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE: event_type and username are strings.\n# @POST: Security event is written to the application log.\n# @RELATION USES -> logger\ndef log_security_event(event_type: str, username: str, details: dict = None):\n with belief_scope(\"log_security_event\", f\"{event_type}:{username}\"):\n timestamp = datetime.utcnow().isoformat()\n msg = f\"[AUDIT][{timestamp}][{event_type}] User: {username}\"\n if details:\n msg += f\" Details: {details}\"\n logger.info(msg)\n# #endregion log_security_event\n\n# #endregion AuthLoggerModule\n" }, { "contract_id": "log_security_event", "contract_type": "Function", "file_path": "backend/src/core/auth/logger.py", - "start_line": 14, - "end_line": 26, + "start_line": 20, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -29428,7 +28934,7 @@ "metadata": { "COMPLEXITY": 3, "INVARIANT": "A single engine instance is used for the entire application.", - "LAYER": "Core", + "LAYER": "Infrastructure", "PURPOSE": "Configures database connection and session management (PostgreSQL-first)." }, "relations": [ @@ -29464,7 +28970,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatabaseModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, connection, session]\n#\n# @BRIEF Configures database connection and session management (PostgreSQL-first).\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [auth_config]\n# @RELATION DEPENDS_ON -> [ConnectionConfig]\n#\n# @INVARIANT: A single engine instance is used for the entire application.\n\nimport os\nfrom pathlib import Path\n\nfrom sqlalchemy import create_engine, inspect, text\nfrom sqlalchemy.orm import sessionmaker\n\nfrom ..models import assistant as _assistant_models # noqa: F401\nfrom ..models import auth as _auth_models # noqa: F401\nfrom ..models import clean_release as _clean_release_models # noqa: F401\nfrom ..models import config as _config_models # noqa: F401\nfrom ..models import connection as _connection_models # noqa: F401\nfrom ..models import dataset_review as _dataset_review_models # noqa: F401\nfrom ..models import llm as _llm_models # noqa: F401\nfrom ..models import profile as _profile_models # noqa: F401\n\n# Import models to ensure they're registered with Base\nfrom ..models import task as _task_models # noqa: F401\nfrom ..models.connection import ConnectionConfig\nfrom ..models.mapping import Base\nfrom .auth.config import auth_config\nfrom .logger import belief_scope, logger\n\n# #region BASE_DIR [C:1] [TYPE Variable]\n# @BRIEF Base directory for the backend.\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\n# #endregion BASE_DIR\n\n# #region DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the main application database.\nDEFAULT_POSTGRES_URL = os.getenv(\n \"POSTGRES_URL\",\n \"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n)\nDATABASE_URL = os.getenv(\"DATABASE_URL\", DEFAULT_POSTGRES_URL)\n# #endregion DATABASE_URL\n\n# #region TASKS_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the tasks execution database.\n# Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance.\nTASKS_DATABASE_URL = os.getenv(\"TASKS_DATABASE_URL\", DATABASE_URL)\n# #endregion TASKS_DATABASE_URL\n\n# #region AUTH_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the authentication database.\nAUTH_DATABASE_URL = os.getenv(\"AUTH_DATABASE_URL\", auth_config.AUTH_DATABASE_URL)\n# #endregion AUTH_DATABASE_URL\n\n\n# #region engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for mappings database.\n# @SIDE_EFFECT: Creates database engine and manages connection pool.\ndef _build_engine(db_url: str):\n with belief_scope(\"_build_engine\"):\n if db_url.startswith(\"sqlite\"):\n return create_engine(db_url, connect_args={\"check_same_thread\": False})\n return create_engine(db_url, pool_pre_ping=True)\n\n\nengine = _build_engine(DATABASE_URL)\n# #endregion engine\n\n# #region tasks_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for tasks database.\ntasks_engine = _build_engine(TASKS_DATABASE_URL)\n# #endregion tasks_engine\n\n# #region auth_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for authentication database.\nauth_engine = _build_engine(AUTH_DATABASE_URL)\n# #endregion auth_engine\n\n# #region SessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the main mappings database.\n# @PRE: engine is initialized.\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# #endregion SessionLocal\n\n# #region TasksSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the tasks execution database.\n# @PRE: tasks_engine is initialized.\nTasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)\n# #endregion TasksSessionLocal\n\n# #region AuthSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the authentication database.\n# @PRE: auth_engine is initialized.\nAuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)\n# #endregion AuthSessionLocal\n\n\n# #region _ensure_user_dashboard_preferences_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table.\n# @PRE: bind_engine points to application database where profile table is stored.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"git_username\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_username VARCHAR\"\n )\n if \"git_email\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_email VARCHAR\"\n )\n if \"git_personal_access_token_encrypted\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN git_personal_access_token_encrypted VARCHAR\"\n )\n if \"start_page\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN start_page VARCHAR NOT NULL DEFAULT 'dashboards'\"\n )\n if \"auto_open_task_drawer\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN auto_open_task_drawer BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n if \"dashboards_table_density\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'\"\n )\n if \"show_only_slug_dashboards\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_columns\n\n\n# #region _ensure_user_dashboard_preferences_health_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table (health fields).\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_health_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_health_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"telegram_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN telegram_id VARCHAR\"\n )\n if \"email_address\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN email_address VARCHAR\"\n )\n if \"notify_on_fail\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN notify_on_fail BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile health preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_health_columns\n\n\n# #region _ensure_llm_validation_results_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for llm_validation_results table.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_llm_validation_results_columns(bind_engine):\n with belief_scope(\"_ensure_llm_validation_results_columns\"):\n table_name = \"llm_validation_results\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"task_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN task_id VARCHAR\"\n )\n if \"environment_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN environment_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"ValidationRecord additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_llm_validation_results_columns\n\n\n# #region _ensure_git_server_configs_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for git_server_configs table.\n# @PRE: bind_engine points to application database.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_git_server_configs_columns(bind_engine):\n with belief_scope(\"_ensure_git_server_configs_columns\"):\n table_name = \"git_server_configs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"default_branch\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE git_server_configs ADD COLUMN default_branch VARCHAR NOT NULL DEFAULT 'main'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"GitServerConfig preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_git_server_configs_columns\n\n\n# #region _ensure_auth_users_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for auth users table.\n# @PRE: bind_engine points to authentication database.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [auth_engine]\ndef _ensure_auth_users_columns(bind_engine):\n with belief_scope(\"_ensure_auth_users_columns\"):\n table_name = \"users\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"full_name\" not in existing_columns:\n alter_statements.append(\"ALTER TABLE users ADD COLUMN full_name VARCHAR\")\n if \"is_ad_user\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE users ADD COLUMN is_ad_user BOOLEAN NOT NULL DEFAULT FALSE\"\n )\n\n if not alter_statements:\n logger.reason(\n \"Auth users schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n return\n\n logger.reason(\n \"Applying additive auth users schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reason(\n \"Auth users schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Auth users additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_auth_users_columns\n\n\n# #region ensure_connection_configs_table [C:3] [TYPE Function]\n# @BRIEF Ensures the external connection registry table exists in the main database.\n# @PRE: bind_engine points to the application database.\n# @POST: connection_configs table exists without dropping existing data.\n# @RELATION DEPENDS_ON -> [ConnectionConfig]\ndef ensure_connection_configs_table(bind_engine):\n with belief_scope(\"ensure_connection_configs_table\"):\n try:\n ConnectionConfig.__table__.create(bind=bind_engine, checkfirst=True)\n except Exception as migration_error:\n logger.explore(\n \"ConnectionConfig table ensure failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion ensure_connection_configs_table\n\n\n# #region _ensure_filter_source_enum_values [C:3] [TYPE Function]\n# @BRIEF Adds missing FilterSource enum values to the PostgreSQL native filtersource type.\n# @PRE: bind_engine points to application database with imported_filters table.\n# @POST: New enum values are available without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_filter_source_enum_values(bind_engine):\n with belief_scope(\"_ensure_filter_source_enum_values\"):\n try:\n with bind_engine.connect() as connection:\n # Check if the native enum type exists\n result = connection.execute(\n text(\n \"SELECT t.typname FROM pg_type t \"\n \"JOIN pg_namespace n ON t.typnamespace = n.oid \"\n \"WHERE t.typname = 'filtersource' AND n.nspname = 'public'\"\n )\n )\n if result.fetchone() is None:\n logger.reason(\n \"filtersource enum type does not exist yet; skipping migration\"\n )\n return\n\n # Get existing enum values\n result = connection.execute(\n text(\n \"SELECT e.enumlabel FROM pg_enum e \"\n \"JOIN pg_type t ON e.enumtypid = t.oid \"\n \"WHERE t.typname = 'filtersource' \"\n \"ORDER BY e.enumsortorder\"\n )\n )\n existing_values = {row[0] for row in result.fetchall()}\n\n required_values = [\"SUPERSET_PERMALINK\", \"SUPERSET_NATIVE_FILTERS_KEY\"]\n missing_values = [\n v for v in required_values if v not in existing_values\n ]\n\n if not missing_values:\n logger.reason(\n \"filtersource enum already up to date\",\n extra={\"existing\": sorted(existing_values)},\n )\n return\n\n logger.reason(\n \"Adding missing values to filtersource enum\",\n extra={\"missing\": missing_values},\n )\n for value in missing_values:\n connection.execute(\n text(\n f\"ALTER TYPE filtersource ADD VALUE IF NOT EXISTS '{value}'\"\n )\n )\n connection.commit()\n logger.reason(\n \"filtersource enum migration completed\",\n extra={\"added\": missing_values},\n )\n except Exception as migration_error:\n logger.explore(\n \"FilterSource enum additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_filter_source_enum_values\n\n\n# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE: bind_engine points to the application database where dataset review tables are stored.\n# @POST: Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT: Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n\n\n# #region _ensure_translation_schedules_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for translation_schedules table.\n# @PRE: bind_engine points to application database.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_translation_schedules_columns(bind_engine):\n with belief_scope(\"_ensure_translation_schedules_columns\"):\n table_name = \"translation_schedules\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"execution_mode\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE translation_schedules \"\n \"ADD COLUMN execution_mode VARCHAR NOT NULL DEFAULT 'full'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"TranslationSchedule additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_translation_schedules_columns\n\n\n# #region _ensure_dictionary_entries_columns [C:3] [TYPE Function]\n# @BRIEF Additive migration for dictionary_entries origin tracking columns.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_dictionary_entries_columns(bind_engine):\n with belief_scope(\"_ensure_dictionary_entries_columns\"):\n table_name = \"dictionary_entries\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"origin_run_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_run_id VARCHAR\"\n )\n if \"origin_row_key\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_row_key VARCHAR\"\n )\n if \"origin_user_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_user_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"DictionaryEntry additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_dictionary_entries_columns\n\n\n# #region init_db [C:3] [TYPE Function]\n# @BRIEF Initializes the database by creating all tables.\n# @PRE: engine, tasks_engine and auth_engine are initialized.\n# @POST: Database tables created in all databases.\n# @SIDE_EFFECT: Creates physical database files if they don't exist.\n# @RELATION CALLS -> [ensure_connection_configs_table]\n# @RELATION CALLS -> [_ensure_filter_source_enum_values]\n# @RELATION CALLS -> [_ensure_dataset_review_session_columns]\n# @RELATION CALLS -> [_ensure_dictionary_entries_columns]\ndef init_db():\n with belief_scope(\"init_db\"):\n Base.metadata.create_all(bind=engine)\n Base.metadata.create_all(bind=tasks_engine)\n Base.metadata.create_all(bind=auth_engine)\n _ensure_user_dashboard_preferences_columns(engine)\n _ensure_llm_validation_results_columns(engine)\n _ensure_user_dashboard_preferences_health_columns(engine)\n _ensure_git_server_configs_columns(engine)\n _ensure_auth_users_columns(auth_engine)\n ensure_connection_configs_table(engine)\n _ensure_filter_source_enum_values(engine)\n _ensure_dataset_review_session_columns(engine)\n _ensure_translation_jobs_columns(engine)\n _ensure_translation_schedules_columns(engine)\n _ensure_dictionary_entries_columns(engine)\n\n\n# #endregion init_db\n\n\n# #region get_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a database session.\n# @PRE: SessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RELATION DEPENDS_ON -> [SessionLocal]\ndef get_db():\n with belief_scope(\"get_db\"):\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_db\n\n\n# #region get_tasks_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a tasks database session.\n# @PRE: TasksSessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\ndef get_tasks_db():\n with belief_scope(\"get_tasks_db\"):\n db = TasksSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_tasks_db\n\n\n# #region get_auth_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting an authentication database session.\n# @PRE: AuthSessionLocal is initialized.\n# @POST: Session is closed after use.\n# @DATA_CONTRACT: None -> Output[sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [AuthSessionLocal]\ndef get_auth_db():\n with belief_scope(\"get_auth_db\"):\n db = AuthSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_auth_db\n\n# #endregion DatabaseModule\n" + "body": "# #region DatabaseModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, connection, session]\n#\n# @BRIEF Configures database connection and session management (PostgreSQL-first).\n# @LAYER: Infrastructure\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [auth_config]\n# @RELATION DEPENDS_ON -> [ConnectionConfig]\n#\n# @INVARIANT: A single engine instance is used for the entire application.\n\nimport os\nfrom pathlib import Path\n\nfrom sqlalchemy import create_engine, inspect, text\nfrom sqlalchemy.orm import sessionmaker\n\nfrom ..models import assistant as _assistant_models # noqa: F401\nfrom ..models import auth as _auth_models # noqa: F401\nfrom ..models import clean_release as _clean_release_models # noqa: F401\nfrom ..models import config as _config_models # noqa: F401\nfrom ..models import connection as _connection_models # noqa: F401\nfrom ..models import dataset_review as _dataset_review_models # noqa: F401\nfrom ..models import llm as _llm_models # noqa: F401\nfrom ..models import profile as _profile_models # noqa: F401\n\n# Import models to ensure they're registered with Base\nfrom ..models import task as _task_models # noqa: F401\nfrom ..models.connection import ConnectionConfig\nfrom ..models.mapping import Base\nfrom .auth.config import auth_config\nfrom .logger import belief_scope, logger\n\n# #region BASE_DIR [C:1] [TYPE Variable]\n# @BRIEF Base directory for the backend.\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\n# #endregion BASE_DIR\n\n# #region DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the main application database.\nDEFAULT_POSTGRES_URL = os.getenv(\n \"POSTGRES_URL\",\n \"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n)\nDATABASE_URL = os.getenv(\"DATABASE_URL\", DEFAULT_POSTGRES_URL)\n# #endregion DATABASE_URL\n\n# #region TASKS_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the tasks execution database.\n# Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance.\nTASKS_DATABASE_URL = os.getenv(\"TASKS_DATABASE_URL\", DATABASE_URL)\n# #endregion TASKS_DATABASE_URL\n\n# #region AUTH_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the authentication database.\nAUTH_DATABASE_URL = os.getenv(\"AUTH_DATABASE_URL\", auth_config.AUTH_DATABASE_URL)\n# #endregion AUTH_DATABASE_URL\n\n\n# #region engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for mappings database.\n# @SIDE_EFFECT: Creates database engine and manages connection pool.\ndef _build_engine(db_url: str):\n with belief_scope(\"_build_engine\"):\n if db_url.startswith(\"sqlite\"):\n return create_engine(db_url, connect_args={\"check_same_thread\": False})\n return create_engine(db_url, pool_pre_ping=True)\n\n\nengine = _build_engine(DATABASE_URL)\n# #endregion engine\n\n# #region tasks_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for tasks database.\ntasks_engine = _build_engine(TASKS_DATABASE_URL)\n# #endregion tasks_engine\n\n# #region auth_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for authentication database.\nauth_engine = _build_engine(AUTH_DATABASE_URL)\n# #endregion auth_engine\n\n# #region SessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the main mappings database.\n# @PRE: engine is initialized.\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# #endregion SessionLocal\n\n# #region TasksSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the tasks execution database.\n# @PRE: tasks_engine is initialized.\nTasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)\n# #endregion TasksSessionLocal\n\n# #region AuthSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the authentication database.\n# @PRE: auth_engine is initialized.\nAuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)\n# #endregion AuthSessionLocal\n\n\n# #region _ensure_user_dashboard_preferences_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table.\n# @PRE: bind_engine points to application database where profile table is stored.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"git_username\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_username VARCHAR\"\n )\n if \"git_email\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_email VARCHAR\"\n )\n if \"git_personal_access_token_encrypted\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN git_personal_access_token_encrypted VARCHAR\"\n )\n if \"start_page\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN start_page VARCHAR NOT NULL DEFAULT 'dashboards'\"\n )\n if \"auto_open_task_drawer\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN auto_open_task_drawer BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n if \"dashboards_table_density\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'\"\n )\n if \"show_only_slug_dashboards\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_columns\n\n\n# #region _ensure_user_dashboard_preferences_health_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table (health fields).\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_health_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_health_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"telegram_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN telegram_id VARCHAR\"\n )\n if \"email_address\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN email_address VARCHAR\"\n )\n if \"notify_on_fail\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN notify_on_fail BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile health preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_health_columns\n\n\n# #region _ensure_llm_validation_results_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for llm_validation_results table.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_llm_validation_results_columns(bind_engine):\n with belief_scope(\"_ensure_llm_validation_results_columns\"):\n table_name = \"llm_validation_results\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"task_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN task_id VARCHAR\"\n )\n if \"environment_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN environment_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"ValidationRecord additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_llm_validation_results_columns\n\n\n# #region _ensure_git_server_configs_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for git_server_configs table.\n# @PRE: bind_engine points to application database.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_git_server_configs_columns(bind_engine):\n with belief_scope(\"_ensure_git_server_configs_columns\"):\n table_name = \"git_server_configs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"default_branch\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE git_server_configs ADD COLUMN default_branch VARCHAR NOT NULL DEFAULT 'main'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"GitServerConfig preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_git_server_configs_columns\n\n\n# #region _ensure_auth_users_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for auth users table.\n# @PRE: bind_engine points to authentication database.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [auth_engine]\ndef _ensure_auth_users_columns(bind_engine):\n with belief_scope(\"_ensure_auth_users_columns\"):\n table_name = \"users\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"full_name\" not in existing_columns:\n alter_statements.append(\"ALTER TABLE users ADD COLUMN full_name VARCHAR\")\n if \"is_ad_user\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE users ADD COLUMN is_ad_user BOOLEAN NOT NULL DEFAULT FALSE\"\n )\n\n if not alter_statements:\n logger.reason(\n \"Auth users schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n return\n\n logger.reason(\n \"Applying additive auth users schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reason(\n \"Auth users schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Auth users additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_auth_users_columns\n\n\n# #region ensure_connection_configs_table [C:3] [TYPE Function]\n# @BRIEF Ensures the external connection registry table exists in the main database.\n# @PRE: bind_engine points to the application database.\n# @POST: connection_configs table exists without dropping existing data.\n# @RELATION DEPENDS_ON -> [ConnectionConfig]\ndef ensure_connection_configs_table(bind_engine):\n with belief_scope(\"ensure_connection_configs_table\"):\n try:\n ConnectionConfig.__table__.create(bind=bind_engine, checkfirst=True)\n except Exception as migration_error:\n logger.explore(\n \"ConnectionConfig table ensure failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion ensure_connection_configs_table\n\n\n# #region _ensure_filter_source_enum_values [C:3] [TYPE Function]\n# @BRIEF Adds missing FilterSource enum values to the PostgreSQL native filtersource type.\n# @PRE: bind_engine points to application database with imported_filters table.\n# @POST: New enum values are available without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_filter_source_enum_values(bind_engine):\n with belief_scope(\"_ensure_filter_source_enum_values\"):\n try:\n with bind_engine.connect() as connection:\n # Check if the native enum type exists\n result = connection.execute(\n text(\n \"SELECT t.typname FROM pg_type t \"\n \"JOIN pg_namespace n ON t.typnamespace = n.oid \"\n \"WHERE t.typname = 'filtersource' AND n.nspname = 'public'\"\n )\n )\n if result.fetchone() is None:\n logger.reason(\n \"filtersource enum type does not exist yet; skipping migration\"\n )\n return\n\n # Get existing enum values\n result = connection.execute(\n text(\n \"SELECT e.enumlabel FROM pg_enum e \"\n \"JOIN pg_type t ON e.enumtypid = t.oid \"\n \"WHERE t.typname = 'filtersource' \"\n \"ORDER BY e.enumsortorder\"\n )\n )\n existing_values = {row[0] for row in result.fetchall()}\n\n required_values = [\"SUPERSET_PERMALINK\", \"SUPERSET_NATIVE_FILTERS_KEY\"]\n missing_values = [\n v for v in required_values if v not in existing_values\n ]\n\n if not missing_values:\n logger.reason(\n \"filtersource enum already up to date\",\n extra={\"existing\": sorted(existing_values)},\n )\n return\n\n logger.reason(\n \"Adding missing values to filtersource enum\",\n extra={\"missing\": missing_values},\n )\n for value in missing_values:\n connection.execute(\n text(\n f\"ALTER TYPE filtersource ADD VALUE IF NOT EXISTS '{value}'\"\n )\n )\n connection.commit()\n logger.reason(\n \"filtersource enum migration completed\",\n extra={\"added\": missing_values},\n )\n except Exception as migration_error:\n logger.explore(\n \"FilterSource enum additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_filter_source_enum_values\n\n\n# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE: bind_engine points to the application database where dataset review tables are stored.\n# @POST: Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT: Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n\n\n# #region _ensure_translation_schedules_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for translation_schedules table.\n# @PRE: bind_engine points to application database.\n# @POST: Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_translation_schedules_columns(bind_engine):\n with belief_scope(\"_ensure_translation_schedules_columns\"):\n table_name = \"translation_schedules\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"execution_mode\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE translation_schedules \"\n \"ADD COLUMN execution_mode VARCHAR NOT NULL DEFAULT 'full'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"TranslationSchedule additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_translation_schedules_columns\n\n\n# #region _ensure_dictionary_entries_columns [C:3] [TYPE Function]\n# @BRIEF Additive migration for dictionary_entries origin tracking columns.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_dictionary_entries_columns(bind_engine):\n with belief_scope(\"_ensure_dictionary_entries_columns\"):\n table_name = \"dictionary_entries\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"origin_run_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_run_id VARCHAR\"\n )\n if \"origin_row_key\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_row_key VARCHAR\"\n )\n if \"origin_user_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_user_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"DictionaryEntry additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_dictionary_entries_columns\n\n\n# #region init_db [C:3] [TYPE Function]\n# @BRIEF Initializes the database by creating all tables.\n# @PRE: engine, tasks_engine and auth_engine are initialized.\n# @POST: Database tables created in all databases.\n# @SIDE_EFFECT: Creates physical database files if they don't exist.\n# @RELATION CALLS -> [ensure_connection_configs_table]\n# @RELATION CALLS -> [_ensure_filter_source_enum_values]\n# @RELATION CALLS -> [_ensure_dataset_review_session_columns]\n# @RELATION CALLS -> [_ensure_dictionary_entries_columns]\ndef init_db():\n with belief_scope(\"init_db\"):\n Base.metadata.create_all(bind=engine)\n Base.metadata.create_all(bind=tasks_engine)\n Base.metadata.create_all(bind=auth_engine)\n _ensure_user_dashboard_preferences_columns(engine)\n _ensure_llm_validation_results_columns(engine)\n _ensure_user_dashboard_preferences_health_columns(engine)\n _ensure_git_server_configs_columns(engine)\n _ensure_auth_users_columns(auth_engine)\n ensure_connection_configs_table(engine)\n _ensure_filter_source_enum_values(engine)\n _ensure_dataset_review_session_columns(engine)\n _ensure_translation_jobs_columns(engine)\n _ensure_translation_schedules_columns(engine)\n _ensure_dictionary_entries_columns(engine)\n\n\n# #endregion init_db\n\n\n# #region get_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a database session.\n# @PRE: SessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RELATION DEPENDS_ON -> [SessionLocal]\ndef get_db():\n with belief_scope(\"get_db\"):\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_db\n\n\n# #region get_tasks_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a tasks database session.\n# @PRE: TasksSessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\ndef get_tasks_db():\n with belief_scope(\"get_tasks_db\"):\n db = TasksSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_tasks_db\n\n\n# #region get_auth_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting an authentication database session.\n# @PRE: AuthSessionLocal is initialized.\n# @POST: Session is closed after use.\n# @DATA_CONTRACT: None -> Output[sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [AuthSessionLocal]\ndef get_auth_db():\n with belief_scope(\"get_auth_db\"):\n db = AuthSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_auth_db\n\n# #endregion DatabaseModule\n" }, { "contract_id": "BASE_DIR", @@ -32282,14 +31788,18 @@ "contract_type": "Module", "file_path": "backend/src/core/migration/archive_parser.py", "start_line": 1, - "end_line": 141, - "tier": "TIER_2", - "complexity": 3, + "end_line": 145, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ArchivePath -> ParsedMigration", "INVARIANT": "Parsing is read-only and never mutates archive files.", - "LAYER": "Core", - "PURPOSE": "Parse Superset export ZIP archives into normalized object catalogs for diffing." + "LAYER": "Domain", + "POST": "Parsed migration archive returned", + "PRE": "Archive file path is valid and readable", + "PURPOSE": "Parse Superset export ZIP archives into normalized object catalogs for diffing.", + "SIDE_EFFECT": "Reads archive file (read-only)" }, "relations": [ { @@ -32299,27 +31809,17 @@ "target_ref": "[LoggerModule]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MigrationArchiveParserModule [C:3] [TYPE Module] [SEMANTICS migration, transform, superset, migration-archive-parser]\n# @BRIEF Parse Superset export ZIP archives into normalized object catalogs for diffing.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Parsing is read-only and never mutates archive files.\nimport json\nimport tempfile\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any\n\nimport yaml\n\nfrom ..logger import belief_scope, logger\n\n\n# #region MigrationArchiveParser [TYPE Class]\n# @BRIEF Extract normalized dashboards/charts/datasets metadata from ZIP archives.\n# @RELATION CONTAINS -> [extract_objects_from_zip]\n# @RELATION CONTAINS -> [_collect_yaml_objects]\n# @RELATION CONTAINS -> [_normalize_object_payload]\nclass MigrationArchiveParser:\n # #region extract_objects_from_zip [TYPE Function]\n # @PURPOSE: Extract object catalogs from Superset archive.\n # @RELATION: DEPENDS_ON -> [_collect_yaml_objects]\n # @PRE: zip_path points to a valid readable ZIP.\n # @POST: Returns object lists grouped by resource type.\n # @RETURN: Dict[str, List[Dict[str, Any]]]\n def extract_objects_from_zip(\n self, zip_path: str\n ) -> dict[str, list[dict[str, Any]]]:\n with belief_scope(\"MigrationArchiveParser.extract_objects_from_zip\"):\n result: dict[str, list[dict[str, Any]]] = {\n \"dashboards\": [],\n \"charts\": [],\n \"datasets\": [],\n }\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n with zipfile.ZipFile(zip_path, \"r\") as zip_file:\n zip_file.extractall(temp_dir)\n result[\"dashboards\"] = self._collect_yaml_objects(\n temp_dir, \"dashboards\"\n )\n result[\"charts\"] = self._collect_yaml_objects(temp_dir, \"charts\")\n result[\"datasets\"] = self._collect_yaml_objects(temp_dir, \"datasets\")\n return result\n # #endregion extract_objects_from_zip\n # #region _collect_yaml_objects [TYPE Function]\n # @PURPOSE: Read and normalize YAML manifests for one object type.\n # @RELATION: DEPENDS_ON -> [_normalize_object_payload]\n # @PRE: object_type is one of dashboards/charts/datasets.\n # @POST: Returns only valid normalized objects.\n def _collect_yaml_objects(\n self, root_dir: Path, object_type: str\n ) -> list[dict[str, Any]]:\n with belief_scope(\"MigrationArchiveParser._collect_yaml_objects\"):\n files = list(root_dir.glob(f\"**/{object_type}/**/*.yaml\")) + list(\n root_dir.glob(f\"**/{object_type}/*.yaml\")\n )\n objects: list[dict[str, Any]] = []\n for file_path in set(files):\n try:\n with open(file_path) as file_obj:\n payload = yaml.safe_load(file_obj) or {}\n normalized = self._normalize_object_payload(payload, object_type)\n if normalized:\n objects.append(normalized)\n except Exception as exc:\n logger.reflect(\n f\"skip_invalid_yaml path={file_path} error={exc}\",\n extra={\"src\": \"MigrationArchiveParser._collect_yaml_objects\"},\n )\n return objects\n # #endregion _collect_yaml_objects\n # #region _normalize_object_payload [TYPE Function]\n # @PURPOSE: Convert raw YAML payload to stable diff signature shape.\n # @PRE: payload is parsed YAML mapping.\n # @POST: Returns normalized descriptor with `uuid`, `title`, and `signature`.\n def _normalize_object_payload(\n self, payload: dict[str, Any], object_type: str\n ) -> dict[str, Any] | None:\n with belief_scope(\"MigrationArchiveParser._normalize_object_payload\"):\n if not isinstance(payload, dict):\n return None\n uuid = payload.get(\"uuid\")\n if not uuid:\n return None\n if object_type == \"dashboards\":\n title = payload.get(\"dashboard_title\") or payload.get(\"title\")\n signature = {\n \"title\": title,\n \"slug\": payload.get(\"slug\"),\n \"position_json\": payload.get(\"position_json\"),\n \"json_metadata\": payload.get(\"json_metadata\"),\n \"description\": payload.get(\"description\"),\n \"owners\": payload.get(\"owners\"),\n }\n return {\n \"uuid\": str(uuid),\n \"title\": title or f\"Dashboard {uuid}\",\n \"signature\": json.dumps(signature, sort_keys=True, default=str),\n \"owners\": payload.get(\"owners\") or [],\n }\n if object_type == \"charts\":\n title = payload.get(\"slice_name\") or payload.get(\"name\")\n signature = {\n \"title\": title,\n \"viz_type\": payload.get(\"viz_type\"),\n \"params\": payload.get(\"params\"),\n \"query_context\": payload.get(\"query_context\"),\n \"datasource_uuid\": payload.get(\"datasource_uuid\"),\n \"dataset_uuid\": payload.get(\"dataset_uuid\"),\n }\n return {\n \"uuid\": str(uuid),\n \"title\": title or f\"Chart {uuid}\",\n \"signature\": json.dumps(signature, sort_keys=True, default=str),\n \"dataset_uuid\": payload.get(\"datasource_uuid\")\n or payload.get(\"dataset_uuid\"),\n }\n if object_type == \"datasets\":\n title = payload.get(\"table_name\") or payload.get(\"name\")\n signature = {\n \"title\": title,\n \"schema\": payload.get(\"schema\"),\n \"database_uuid\": payload.get(\"database_uuid\"),\n \"sql\": payload.get(\"sql\"),\n \"columns\": payload.get(\"columns\"),\n \"metrics\": payload.get(\"metrics\"),\n }\n return {\n \"uuid\": str(uuid),\n \"title\": title or f\"Dataset {uuid}\",\n \"signature\": json.dumps(signature, sort_keys=True, default=str),\n \"database_uuid\": payload.get(\"database_uuid\"),\n }\n return None\n # #endregion _normalize_object_payload\n# #endregion MigrationArchiveParser\n# #endregion MigrationArchiveParserModule\n" + "body": "# #region MigrationArchiveParserModule [C:5] [TYPE Module] [SEMANTICS migration, transform, superset, migration-archive-parser]\n# @BRIEF Parse Superset export ZIP archives into normalized object catalogs for diffing.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Parsing is read-only and never mutates archive files.\n# @PRE: Archive file path is valid and readable\n# @POST: Parsed migration archive returned\n# @SIDE_EFFECT: Reads archive file (read-only)\n# @DATA_CONTRACT: ArchivePath -> ParsedMigration\nimport json\nimport tempfile\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any\n\nimport yaml\n\nfrom ..logger import belief_scope, logger\n\n\n# #region MigrationArchiveParser [TYPE Class]\n# @BRIEF Extract normalized dashboards/charts/datasets metadata from ZIP archives.\n# @RELATION CONTAINS -> [extract_objects_from_zip]\n# @RELATION CONTAINS -> [_collect_yaml_objects]\n# @RELATION CONTAINS -> [_normalize_object_payload]\nclass MigrationArchiveParser:\n # #region extract_objects_from_zip [TYPE Function]\n # @PURPOSE: Extract object catalogs from Superset archive.\n # @RELATION: DEPENDS_ON -> [_collect_yaml_objects]\n # @PRE: zip_path points to a valid readable ZIP.\n # @POST: Returns object lists grouped by resource type.\n # @RETURN: Dict[str, List[Dict[str, Any]]]\n def extract_objects_from_zip(\n self, zip_path: str\n ) -> dict[str, list[dict[str, Any]]]:\n with belief_scope(\"MigrationArchiveParser.extract_objects_from_zip\"):\n result: dict[str, list[dict[str, Any]]] = {\n \"dashboards\": [],\n \"charts\": [],\n \"datasets\": [],\n }\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n with zipfile.ZipFile(zip_path, \"r\") as zip_file:\n zip_file.extractall(temp_dir)\n result[\"dashboards\"] = self._collect_yaml_objects(\n temp_dir, \"dashboards\"\n )\n result[\"charts\"] = self._collect_yaml_objects(temp_dir, \"charts\")\n result[\"datasets\"] = self._collect_yaml_objects(temp_dir, \"datasets\")\n return result\n # #endregion extract_objects_from_zip\n # #region _collect_yaml_objects [TYPE Function]\n # @PURPOSE: Read and normalize YAML manifests for one object type.\n # @RELATION: DEPENDS_ON -> [_normalize_object_payload]\n # @PRE: object_type is one of dashboards/charts/datasets.\n # @POST: Returns only valid normalized objects.\n def _collect_yaml_objects(\n self, root_dir: Path, object_type: str\n ) -> list[dict[str, Any]]:\n with belief_scope(\"MigrationArchiveParser._collect_yaml_objects\"):\n files = list(root_dir.glob(f\"**/{object_type}/**/*.yaml\")) + list(\n root_dir.glob(f\"**/{object_type}/*.yaml\")\n )\n objects: list[dict[str, Any]] = []\n for file_path in set(files):\n try:\n with open(file_path) as file_obj:\n payload = yaml.safe_load(file_obj) or {}\n normalized = self._normalize_object_payload(payload, object_type)\n if normalized:\n objects.append(normalized)\n except Exception as exc:\n logger.reflect(\n f\"skip_invalid_yaml path={file_path} error={exc}\",\n extra={\"src\": \"MigrationArchiveParser._collect_yaml_objects\"},\n )\n return objects\n # #endregion _collect_yaml_objects\n # #region _normalize_object_payload [TYPE Function]\n # @PURPOSE: Convert raw YAML payload to stable diff signature shape.\n # @PRE: payload is parsed YAML mapping.\n # @POST: Returns normalized descriptor with `uuid`, `title`, and `signature`.\n def _normalize_object_payload(\n self, payload: dict[str, Any], object_type: str\n ) -> dict[str, Any] | None:\n with belief_scope(\"MigrationArchiveParser._normalize_object_payload\"):\n if not isinstance(payload, dict):\n return None\n uuid = payload.get(\"uuid\")\n if not uuid:\n return None\n if object_type == \"dashboards\":\n title = payload.get(\"dashboard_title\") or payload.get(\"title\")\n signature = {\n \"title\": title,\n \"slug\": payload.get(\"slug\"),\n \"position_json\": payload.get(\"position_json\"),\n \"json_metadata\": payload.get(\"json_metadata\"),\n \"description\": payload.get(\"description\"),\n \"owners\": payload.get(\"owners\"),\n }\n return {\n \"uuid\": str(uuid),\n \"title\": title or f\"Dashboard {uuid}\",\n \"signature\": json.dumps(signature, sort_keys=True, default=str),\n \"owners\": payload.get(\"owners\") or [],\n }\n if object_type == \"charts\":\n title = payload.get(\"slice_name\") or payload.get(\"name\")\n signature = {\n \"title\": title,\n \"viz_type\": payload.get(\"viz_type\"),\n \"params\": payload.get(\"params\"),\n \"query_context\": payload.get(\"query_context\"),\n \"datasource_uuid\": payload.get(\"datasource_uuid\"),\n \"dataset_uuid\": payload.get(\"dataset_uuid\"),\n }\n return {\n \"uuid\": str(uuid),\n \"title\": title or f\"Chart {uuid}\",\n \"signature\": json.dumps(signature, sort_keys=True, default=str),\n \"dataset_uuid\": payload.get(\"datasource_uuid\")\n or payload.get(\"dataset_uuid\"),\n }\n if object_type == \"datasets\":\n title = payload.get(\"table_name\") or payload.get(\"name\")\n signature = {\n \"title\": title,\n \"schema\": payload.get(\"schema\"),\n \"database_uuid\": payload.get(\"database_uuid\"),\n \"sql\": payload.get(\"sql\"),\n \"columns\": payload.get(\"columns\"),\n \"metrics\": payload.get(\"metrics\"),\n }\n return {\n \"uuid\": str(uuid),\n \"title\": title or f\"Dataset {uuid}\",\n \"signature\": json.dumps(signature, sort_keys=True, default=str),\n \"database_uuid\": payload.get(\"database_uuid\"),\n }\n return None\n # #endregion _normalize_object_payload\n# #endregion MigrationArchiveParser\n# #endregion MigrationArchiveParserModule\n" }, { "contract_id": "MigrationArchiveParser", "contract_type": "Class", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 17, - "end_line": 140, + "start_line": 21, + "end_line": 144, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32418,8 +31918,8 @@ "contract_id": "extract_objects_from_zip", "contract_type": "Function", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 23, - "end_line": 48, + "start_line": 27, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32479,8 +31979,8 @@ "contract_id": "_collect_yaml_objects", "contract_type": "Function", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 49, - "end_line": 75, + "start_line": 53, + "end_line": 79, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32533,8 +32033,8 @@ "contract_id": "_normalize_object_payload", "contract_type": "Function", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 76, - "end_line": 139, + "start_line": 80, + "end_line": 143, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32572,14 +32072,18 @@ "contract_type": "Module", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", "start_line": 1, - "end_line": 339, - "tier": "TIER_2", - "complexity": 3, + "end_line": 343, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "EnvironmentConfig -> MigrationDiffReport", "INVARIANT": "Dry run is informative only and must not mutate target environment.", - "LAYER": "Core", - "PURPOSE": "Compute pre-flight migration diff and risk scoring without apply." + "LAYER": "Domain", + "POST": "Dry-run diff returned without mutation", + "PRE": "Source and target environments configured", + "PURPOSE": "Compute pre-flight migration diff and risk scoring without apply.", + "SIDE_EFFECT": "Reads source environment (read-only)" }, "relations": [ { @@ -32607,27 +32111,17 @@ "target_ref": "[RiskAssessorModule]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MigrationDryRunOrchestratorModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, migration, orchestration, diff, migration-dry-run-service]\n# @BRIEF Compute pre-flight migration diff and risk scoring without apply.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [MigrationEngine]\n# @RELATION DEPENDS_ON -> [MigrationArchiveParser]\n# @RELATION DEPENDS_ON -> [RiskAssessorModule]\n# @INVARIANT: Dry run is informative only and must not mutate target environment.\nimport json\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.dashboard import DashboardSelection\nfrom ...models.mapping import DatabaseMapping\nfrom ..logger import belief_scope, logger\nfrom ..migration_engine import MigrationEngine\nfrom ..superset_client import SupersetClient\nfrom ..utils.fileio import create_temp_file\nfrom .archive_parser import MigrationArchiveParser\nfrom .risk_assessor import build_risks, score_risks\n\n\n# #region MigrationDryRunService [TYPE Class]\n# @BRIEF Build deterministic diff/risk payload for migration pre-flight.\n# @RELATION CONTAINS -> [__init__]\n# @RELATION CONTAINS -> [run]\n# @RELATION CONTAINS -> [_load_db_mapping]\n# @RELATION CONTAINS -> [_accumulate_objects]\n# @RELATION CONTAINS -> [_index_by_uuid]\n# @RELATION CONTAINS -> [_build_object_diff]\n# @RELATION CONTAINS -> [_build_target_signatures]\n# @RELATION CONTAINS -> [_build_risks]\nclass MigrationDryRunService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Wire parser dependency for archive object extraction.\n # @PRE: parser can be omitted to use default implementation.\n # @POST: Service is ready to calculate dry-run payload.\n def __init__(self, parser: MigrationArchiveParser | None = None):\n self.parser = parser or MigrationArchiveParser()\n # #endregion __init__\n # #region run [TYPE Function]\n # @PURPOSE: Execute full dry-run computation for selected dashboards.\n # @RELATION: DEPENDS_ON -> [_load_db_mapping]\n # @RELATION: DEPENDS_ON -> [_accumulate_objects]\n # @RELATION: DEPENDS_ON -> [_build_target_signatures]\n # @RELATION: DEPENDS_ON -> [_build_object_diff]\n # @RELATION: DEPENDS_ON -> [_build_risks]\n # @PRE: source/target clients are authenticated and selection validated by caller.\n # @POST: Returns JSON-serializable pre-flight payload with summary, diff and risk.\n # @SIDE_EFFECT: Reads source export archives and target metadata via network.\n def run(\n self,\n selection: DashboardSelection,\n source_client: SupersetClient,\n target_client: SupersetClient,\n db: Session,\n ) -> dict[str, Any]:\n with belief_scope(\"MigrationDryRunService.run\"):\n logger.reason(\n \"Starting dry-run pipeline\",\n extra={\"src\": \"MigrationDryRunService.run\"},\n )\n engine = MigrationEngine()\n db_mapping = (\n self._load_db_mapping(db, selection)\n if selection.replace_db_config\n else {}\n )\n transformed = {\"dashboards\": {}, \"charts\": {}, \"datasets\": {}}\n dashboards_preview = source_client.get_dashboards_summary()\n selected_preview = {\n item[\"id\"]: item\n for item in dashboards_preview\n if item.get(\"id\") in selection.selected_ids\n }\n for dashboard_id in selection.selected_ids:\n exported_content, _ = source_client.export_dashboard(int(dashboard_id))\n with create_temp_file(\n content=exported_content, suffix=\".zip\"\n ) as source_zip, create_temp_file(suffix=\".zip\") as transformed_zip:\n success = engine.transform_zip(\n str(source_zip),\n str(transformed_zip),\n db_mapping,\n strip_databases=False,\n target_env_id=selection.target_env_id,\n fix_cross_filters=selection.fix_cross_filters,\n )\n if not success:\n raise ValueError(\n f\"Failed to transform export archive for dashboard {dashboard_id}\"\n )\n extracted = self.parser.extract_objects_from_zip(\n str(transformed_zip)\n )\n self._accumulate_objects(transformed, extracted)\n source_objects = {\n key: list(value.values()) for key, value in transformed.items()\n }\n target_objects = self._build_target_signatures(target_client)\n diff = {\n \"dashboards\": self._build_object_diff(\n source_objects[\"dashboards\"], target_objects[\"dashboards\"]\n ),\n \"charts\": self._build_object_diff(\n source_objects[\"charts\"], target_objects[\"charts\"]\n ),\n \"datasets\": self._build_object_diff(\n source_objects[\"datasets\"], target_objects[\"datasets\"]\n ),\n }\n risk = self._build_risks(\n source_objects, target_objects, diff, target_client\n )\n summary = {\n \"dashboards\": {\n action: len(diff[\"dashboards\"][action])\n for action in (\"create\", \"update\", \"delete\")\n },\n \"charts\": {\n action: len(diff[\"charts\"][action])\n for action in (\"create\", \"update\", \"delete\")\n },\n \"datasets\": {\n action: len(diff[\"datasets\"][action])\n for action in (\"create\", \"update\", \"delete\")\n },\n \"selected_dashboards\": len(selection.selected_ids),\n }\n selected_titles = [\n selected_preview[dash_id][\"title\"]\n for dash_id in selection.selected_ids\n if dash_id in selected_preview\n ]\n logger.reason(\n \"Dry-run payload assembled\",\n extra={\"src\": \"MigrationDryRunService.run\"},\n )\n return {\n \"generated_at\": datetime.now(UTC).isoformat(),\n \"selection\": selection.model_dump(),\n \"selected_dashboard_titles\": selected_titles,\n \"diff\": diff,\n \"summary\": summary,\n \"risk\": score_risks(risk),\n }\n # #endregion run\n # #region _load_db_mapping [TYPE Function]\n # @PURPOSE: Resolve UUID mapping for optional DB config replacement.\n def _load_db_mapping(\n self, db: Session, selection: DashboardSelection\n ) -> dict[str, str]:\n rows = (\n db.query(DatabaseMapping)\n .filter(\n DatabaseMapping.source_env_id == selection.source_env_id,\n DatabaseMapping.target_env_id == selection.target_env_id,\n )\n .all()\n )\n return {row.source_db_uuid: row.target_db_uuid for row in rows}\n # #endregion _load_db_mapping\n # #region _accumulate_objects [TYPE Function]\n # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.\n def _accumulate_objects(\n self,\n target: dict[str, dict[str, dict[str, Any]]],\n source: dict[str, list[dict[str, Any]]],\n ) -> None:\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in source.get(object_type, []):\n uuid = item.get(\"uuid\")\n if uuid:\n target[object_type][str(uuid)] = item\n # #endregion _accumulate_objects\n # #region _index_by_uuid [TYPE Function]\n # @PURPOSE: Build UUID-index map for normalized resources.\n def _index_by_uuid(\n self, objects: list[dict[str, Any]]\n ) -> dict[str, dict[str, Any]]:\n indexed: dict[str, dict[str, Any]] = {}\n for obj in objects:\n uuid = obj.get(\"uuid\")\n if uuid:\n indexed[str(uuid)] = obj\n return indexed\n # #endregion _index_by_uuid\n # #region _build_object_diff [TYPE Function]\n # @PURPOSE: Compute create/update/delete buckets by UUID+signature.\n # @RELATION: DEPENDS_ON -> [_index_by_uuid]\n def _build_object_diff(\n self, source_objects: list[dict[str, Any]], target_objects: list[dict[str, Any]]\n ) -> dict[str, list[dict[str, Any]]]:\n target_index = self._index_by_uuid(target_objects)\n created: list[dict[str, Any]] = []\n updated: list[dict[str, Any]] = []\n deleted: list[dict[str, Any]] = []\n for source_obj in source_objects:\n source_uuid = str(source_obj.get(\"uuid\"))\n target_obj = target_index.get(source_uuid)\n if not target_obj:\n created.append({\"uuid\": source_uuid, \"title\": source_obj.get(\"title\")})\n continue\n if source_obj.get(\"signature\") != target_obj.get(\"signature\"):\n updated.append(\n {\n \"uuid\": source_uuid,\n \"title\": source_obj.get(\"title\"),\n \"target_title\": target_obj.get(\"title\"),\n }\n )\n return {\"create\": created, \"update\": updated, \"delete\": deleted}\n # #endregion _build_object_diff\n # #region _build_target_signatures [TYPE Function]\n # @PURPOSE: Pull target metadata and normalize it into comparable signatures.\n def _build_target_signatures(\n self, client: SupersetClient\n ) -> dict[str, list[dict[str, Any]]]:\n _, dashboards = client.get_dashboards(\n query={\n \"columns\": [\n \"uuid\",\n \"dashboard_title\",\n \"slug\",\n \"position_json\",\n \"json_metadata\",\n \"description\",\n \"owners\",\n ],\n }\n )\n _, datasets = client.get_datasets(\n query={\n \"columns\": [\n \"uuid\",\n \"table_name\",\n \"schema\",\n \"database_uuid\",\n \"sql\",\n \"columns\",\n \"metrics\",\n ],\n }\n )\n _, charts = client.get_charts(\n query={\n \"columns\": [\n \"uuid\",\n \"slice_name\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n \"datasource_uuid\",\n \"dataset_uuid\",\n ],\n }\n )\n return {\n \"dashboards\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"dashboard_title\"),\n \"owners\": item.get(\"owners\") or [],\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"dashboard_title\"),\n \"slug\": item.get(\"slug\"),\n \"position_json\": item.get(\"position_json\"),\n \"json_metadata\": item.get(\"json_metadata\"),\n \"description\": item.get(\"description\"),\n \"owners\": item.get(\"owners\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in dashboards\n if item.get(\"uuid\")\n ],\n \"datasets\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"table_name\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"table_name\"),\n \"schema\": item.get(\"schema\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"sql\": item.get(\"sql\"),\n \"columns\": item.get(\"columns\"),\n \"metrics\": item.get(\"metrics\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in datasets\n if item.get(\"uuid\")\n ],\n \"charts\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"dataset_uuid\": item.get(\"datasource_uuid\")\n or item.get(\"dataset_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"viz_type\": item.get(\"viz_type\"),\n \"params\": item.get(\"params\"),\n \"query_context\": item.get(\"query_context\"),\n \"datasource_uuid\": item.get(\"datasource_uuid\"),\n \"dataset_uuid\": item.get(\"dataset_uuid\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in charts\n if item.get(\"uuid\")\n ],\n }\n # #endregion _build_target_signatures\n # #region _build_risks [TYPE Function]\n # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.\n def _build_risks(\n self,\n source_objects: dict[str, list[dict[str, Any]]],\n target_objects: dict[str, list[dict[str, Any]]],\n diff: dict[str, dict[str, list[dict[str, Any]]]],\n target_client: SupersetClient,\n ) -> list[dict[str, Any]]:\n return build_risks(source_objects, target_objects, diff, target_client)\n # #endregion _build_risks\n# #endregion MigrationDryRunService\n# #endregion MigrationDryRunOrchestratorModule\n" + "body": "# #region MigrationDryRunOrchestratorModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, migration, orchestration, diff, migration-dry-run-service]\n# @BRIEF Compute pre-flight migration diff and risk scoring without apply.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [MigrationEngine]\n# @RELATION DEPENDS_ON -> [MigrationArchiveParser]\n# @RELATION DEPENDS_ON -> [RiskAssessorModule]\n# @INVARIANT: Dry run is informative only and must not mutate target environment.\n# @PRE: Source and target environments configured\n# @POST: Dry-run diff returned without mutation\n# @SIDE_EFFECT: Reads source environment (read-only)\n# @DATA_CONTRACT: EnvironmentConfig -> MigrationDiffReport\nimport json\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.dashboard import DashboardSelection\nfrom ...models.mapping import DatabaseMapping\nfrom ..logger import belief_scope, logger\nfrom ..migration_engine import MigrationEngine\nfrom ..superset_client import SupersetClient\nfrom ..utils.fileio import create_temp_file\nfrom .archive_parser import MigrationArchiveParser\nfrom .risk_assessor import build_risks, score_risks\n\n\n# #region MigrationDryRunService [TYPE Class]\n# @BRIEF Build deterministic diff/risk payload for migration pre-flight.\n# @RELATION CONTAINS -> [__init__]\n# @RELATION CONTAINS -> [run]\n# @RELATION CONTAINS -> [_load_db_mapping]\n# @RELATION CONTAINS -> [_accumulate_objects]\n# @RELATION CONTAINS -> [_index_by_uuid]\n# @RELATION CONTAINS -> [_build_object_diff]\n# @RELATION CONTAINS -> [_build_target_signatures]\n# @RELATION CONTAINS -> [_build_risks]\nclass MigrationDryRunService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Wire parser dependency for archive object extraction.\n # @PRE: parser can be omitted to use default implementation.\n # @POST: Service is ready to calculate dry-run payload.\n def __init__(self, parser: MigrationArchiveParser | None = None):\n self.parser = parser or MigrationArchiveParser()\n # #endregion __init__\n # #region run [TYPE Function]\n # @PURPOSE: Execute full dry-run computation for selected dashboards.\n # @RELATION: DEPENDS_ON -> [_load_db_mapping]\n # @RELATION: DEPENDS_ON -> [_accumulate_objects]\n # @RELATION: DEPENDS_ON -> [_build_target_signatures]\n # @RELATION: DEPENDS_ON -> [_build_object_diff]\n # @RELATION: DEPENDS_ON -> [_build_risks]\n # @PRE: source/target clients are authenticated and selection validated by caller.\n # @POST: Returns JSON-serializable pre-flight payload with summary, diff and risk.\n # @SIDE_EFFECT: Reads source export archives and target metadata via network.\n def run(\n self,\n selection: DashboardSelection,\n source_client: SupersetClient,\n target_client: SupersetClient,\n db: Session,\n ) -> dict[str, Any]:\n with belief_scope(\"MigrationDryRunService.run\"):\n logger.reason(\n \"Starting dry-run pipeline\",\n extra={\"src\": \"MigrationDryRunService.run\"},\n )\n engine = MigrationEngine()\n db_mapping = (\n self._load_db_mapping(db, selection)\n if selection.replace_db_config\n else {}\n )\n transformed = {\"dashboards\": {}, \"charts\": {}, \"datasets\": {}}\n dashboards_preview = source_client.get_dashboards_summary()\n selected_preview = {\n item[\"id\"]: item\n for item in dashboards_preview\n if item.get(\"id\") in selection.selected_ids\n }\n for dashboard_id in selection.selected_ids:\n exported_content, _ = source_client.export_dashboard(int(dashboard_id))\n with create_temp_file(\n content=exported_content, suffix=\".zip\"\n ) as source_zip, create_temp_file(suffix=\".zip\") as transformed_zip:\n success = engine.transform_zip(\n str(source_zip),\n str(transformed_zip),\n db_mapping,\n strip_databases=False,\n target_env_id=selection.target_env_id,\n fix_cross_filters=selection.fix_cross_filters,\n )\n if not success:\n raise ValueError(\n f\"Failed to transform export archive for dashboard {dashboard_id}\"\n )\n extracted = self.parser.extract_objects_from_zip(\n str(transformed_zip)\n )\n self._accumulate_objects(transformed, extracted)\n source_objects = {\n key: list(value.values()) for key, value in transformed.items()\n }\n target_objects = self._build_target_signatures(target_client)\n diff = {\n \"dashboards\": self._build_object_diff(\n source_objects[\"dashboards\"], target_objects[\"dashboards\"]\n ),\n \"charts\": self._build_object_diff(\n source_objects[\"charts\"], target_objects[\"charts\"]\n ),\n \"datasets\": self._build_object_diff(\n source_objects[\"datasets\"], target_objects[\"datasets\"]\n ),\n }\n risk = self._build_risks(\n source_objects, target_objects, diff, target_client\n )\n summary = {\n \"dashboards\": {\n action: len(diff[\"dashboards\"][action])\n for action in (\"create\", \"update\", \"delete\")\n },\n \"charts\": {\n action: len(diff[\"charts\"][action])\n for action in (\"create\", \"update\", \"delete\")\n },\n \"datasets\": {\n action: len(diff[\"datasets\"][action])\n for action in (\"create\", \"update\", \"delete\")\n },\n \"selected_dashboards\": len(selection.selected_ids),\n }\n selected_titles = [\n selected_preview[dash_id][\"title\"]\n for dash_id in selection.selected_ids\n if dash_id in selected_preview\n ]\n logger.reason(\n \"Dry-run payload assembled\",\n extra={\"src\": \"MigrationDryRunService.run\"},\n )\n return {\n \"generated_at\": datetime.now(UTC).isoformat(),\n \"selection\": selection.model_dump(),\n \"selected_dashboard_titles\": selected_titles,\n \"diff\": diff,\n \"summary\": summary,\n \"risk\": score_risks(risk),\n }\n # #endregion run\n # #region _load_db_mapping [TYPE Function]\n # @PURPOSE: Resolve UUID mapping for optional DB config replacement.\n def _load_db_mapping(\n self, db: Session, selection: DashboardSelection\n ) -> dict[str, str]:\n rows = (\n db.query(DatabaseMapping)\n .filter(\n DatabaseMapping.source_env_id == selection.source_env_id,\n DatabaseMapping.target_env_id == selection.target_env_id,\n )\n .all()\n )\n return {row.source_db_uuid: row.target_db_uuid for row in rows}\n # #endregion _load_db_mapping\n # #region _accumulate_objects [TYPE Function]\n # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.\n def _accumulate_objects(\n self,\n target: dict[str, dict[str, dict[str, Any]]],\n source: dict[str, list[dict[str, Any]]],\n ) -> None:\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in source.get(object_type, []):\n uuid = item.get(\"uuid\")\n if uuid:\n target[object_type][str(uuid)] = item\n # #endregion _accumulate_objects\n # #region _index_by_uuid [TYPE Function]\n # @PURPOSE: Build UUID-index map for normalized resources.\n def _index_by_uuid(\n self, objects: list[dict[str, Any]]\n ) -> dict[str, dict[str, Any]]:\n indexed: dict[str, dict[str, Any]] = {}\n for obj in objects:\n uuid = obj.get(\"uuid\")\n if uuid:\n indexed[str(uuid)] = obj\n return indexed\n # #endregion _index_by_uuid\n # #region _build_object_diff [TYPE Function]\n # @PURPOSE: Compute create/update/delete buckets by UUID+signature.\n # @RELATION: DEPENDS_ON -> [_index_by_uuid]\n def _build_object_diff(\n self, source_objects: list[dict[str, Any]], target_objects: list[dict[str, Any]]\n ) -> dict[str, list[dict[str, Any]]]:\n target_index = self._index_by_uuid(target_objects)\n created: list[dict[str, Any]] = []\n updated: list[dict[str, Any]] = []\n deleted: list[dict[str, Any]] = []\n for source_obj in source_objects:\n source_uuid = str(source_obj.get(\"uuid\"))\n target_obj = target_index.get(source_uuid)\n if not target_obj:\n created.append({\"uuid\": source_uuid, \"title\": source_obj.get(\"title\")})\n continue\n if source_obj.get(\"signature\") != target_obj.get(\"signature\"):\n updated.append(\n {\n \"uuid\": source_uuid,\n \"title\": source_obj.get(\"title\"),\n \"target_title\": target_obj.get(\"title\"),\n }\n )\n return {\"create\": created, \"update\": updated, \"delete\": deleted}\n # #endregion _build_object_diff\n # #region _build_target_signatures [TYPE Function]\n # @PURPOSE: Pull target metadata and normalize it into comparable signatures.\n def _build_target_signatures(\n self, client: SupersetClient\n ) -> dict[str, list[dict[str, Any]]]:\n _, dashboards = client.get_dashboards(\n query={\n \"columns\": [\n \"uuid\",\n \"dashboard_title\",\n \"slug\",\n \"position_json\",\n \"json_metadata\",\n \"description\",\n \"owners\",\n ],\n }\n )\n _, datasets = client.get_datasets(\n query={\n \"columns\": [\n \"uuid\",\n \"table_name\",\n \"schema\",\n \"database_uuid\",\n \"sql\",\n \"columns\",\n \"metrics\",\n ],\n }\n )\n _, charts = client.get_charts(\n query={\n \"columns\": [\n \"uuid\",\n \"slice_name\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n \"datasource_uuid\",\n \"dataset_uuid\",\n ],\n }\n )\n return {\n \"dashboards\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"dashboard_title\"),\n \"owners\": item.get(\"owners\") or [],\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"dashboard_title\"),\n \"slug\": item.get(\"slug\"),\n \"position_json\": item.get(\"position_json\"),\n \"json_metadata\": item.get(\"json_metadata\"),\n \"description\": item.get(\"description\"),\n \"owners\": item.get(\"owners\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in dashboards\n if item.get(\"uuid\")\n ],\n \"datasets\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"table_name\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"table_name\"),\n \"schema\": item.get(\"schema\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"sql\": item.get(\"sql\"),\n \"columns\": item.get(\"columns\"),\n \"metrics\": item.get(\"metrics\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in datasets\n if item.get(\"uuid\")\n ],\n \"charts\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"dataset_uuid\": item.get(\"datasource_uuid\")\n or item.get(\"dataset_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"viz_type\": item.get(\"viz_type\"),\n \"params\": item.get(\"params\"),\n \"query_context\": item.get(\"query_context\"),\n \"datasource_uuid\": item.get(\"datasource_uuid\"),\n \"dataset_uuid\": item.get(\"dataset_uuid\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in charts\n if item.get(\"uuid\")\n ],\n }\n # #endregion _build_target_signatures\n # #region _build_risks [TYPE Function]\n # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.\n def _build_risks(\n self,\n source_objects: dict[str, list[dict[str, Any]]],\n target_objects: dict[str, list[dict[str, Any]]],\n diff: dict[str, dict[str, list[dict[str, Any]]]],\n target_client: SupersetClient,\n ) -> list[dict[str, Any]]:\n return build_risks(source_objects, target_objects, diff, target_client)\n # #endregion _build_risks\n# #endregion MigrationDryRunService\n# #endregion MigrationDryRunOrchestratorModule\n" }, { "contract_id": "MigrationDryRunService", "contract_type": "Class", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 25, - "end_line": 338, + "start_line": 29, + "end_line": 342, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32846,8 +32340,8 @@ "contract_id": "_load_db_mapping", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 150, - "end_line": 164, + "start_line": 154, + "end_line": 168, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32863,8 +32357,8 @@ "contract_id": "_accumulate_objects", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 165, - "end_line": 177, + "start_line": 169, + "end_line": 181, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32880,8 +32374,8 @@ "contract_id": "_index_by_uuid", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 178, - "end_line": 189, + "start_line": 182, + "end_line": 193, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32897,8 +32391,8 @@ "contract_id": "_build_object_diff", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 190, - "end_line": 215, + "start_line": 194, + "end_line": 219, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32931,8 +32425,8 @@ "contract_id": "_build_target_signatures", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 216, - "end_line": 326, + "start_line": 220, + "end_line": 330, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32948,8 +32442,8 @@ "contract_id": "_build_risks", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 327, - "end_line": 337, + "start_line": 331, + "end_line": 341, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -35091,13 +34585,13 @@ "contract_type": "Module", "file_path": "backend/src/core/superset_client/__init__.py", "start_line": 1, - "end_line": 68, - "tier": "TIER_2", - "complexity": 3, + "end_line": 72, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "INVARIANT": "All network operations must use the internal APIClient instance.", - "LAYER": "Infra", + "LAYER": "Service", "PUBLIC_API": "SupersetClient", "PURPOSE": "Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию.", "RATIONALE": "Decomposed from monolithic superset_client.py (2145 lines) into" @@ -35135,49 +34629,59 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Infra" - } - }, { "code": "unknown_tag", "tag": "PUBLIC_API", "message": "@PUBLIC_API is not defined in axiom_config.yaml tags", "detail": null + }, + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, package, superset-client]\n# @LAYER: Infra\n# @BRIEF Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию.\n# @RELATION DEPENDS_ON -> [ConfigModels]\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION DEPENDS_ON -> [get_filename_from_headers]\n# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewFiltersMixin]\n#\n# @INVARIANT: All network operations must use the internal APIClient instance.\n# @PUBLIC_API: SupersetClient\n#\n# @RATIONALE: Decomposed from monolithic superset_client.py (2145 lines) into\n# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class\n# preserves the original public API surface — all consumers continue to import\n# `from src.core.superset_client import SupersetClient` without changes.\n# @REJECTED: Keeping a single 2145-line file — violates fractal limit INV_7.\n\nfrom ._base import SupersetClientBase\nfrom ._charts import SupersetChartsMixin\nfrom ._dashboards_crud import SupersetDashboardsCrudMixin\nfrom ._dashboards_filters import SupersetDashboardsFiltersMixin\nfrom ._dashboards_list import SupersetDashboardsListMixin\nfrom ._databases import SupersetDatabasesMixin\nfrom ._datasets import SupersetDatasetsMixin\nfrom ._datasets_preview import SupersetDatasetsPreviewMixin\nfrom ._datasets_preview_filters import SupersetDatasetsPreviewFiltersMixin\nfrom ._user_projection import SupersetUserProjectionMixin\n\n\n# #region SupersetClient [C:3] [TYPE Class]\n# @BRIEF Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами.\n# @RELATION DEPENDS_ON -> [ConfigModels]\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION INHERITS -> [SupersetClientBase]\n# @RELATION INHERITS -> [SupersetUserProjectionMixin]\n# @RELATION INHERITS -> [SupersetDashboardsListMixin]\n# @RELATION INHERITS -> [SupersetDashboardsFiltersMixin]\n# @RELATION INHERITS -> [SupersetDashboardsCrudMixin]\n# @RELATION INHERITS -> [SupersetChartsMixin]\n# @RELATION INHERITS -> [SupersetDatasetsMixin]\n# @RELATION INHERITS -> [SupersetDatasetsPreviewMixin]\n# @RELATION INHERITS -> [SupersetDatasetsPreviewFiltersMixin]\n# @RELATION INHERITS -> [SupersetDatabasesMixin]\nclass SupersetClient(\n SupersetDatabasesMixin,\n SupersetDatasetsPreviewFiltersMixin,\n SupersetDatasetsPreviewMixin,\n SupersetDatasetsMixin,\n SupersetChartsMixin,\n SupersetDashboardsCrudMixin,\n SupersetDashboardsFiltersMixin,\n SupersetDashboardsListMixin,\n SupersetUserProjectionMixin,\n SupersetClientBase,\n):\n \"\"\"Composed Superset REST API client.\n\n MRO order ensures domain mixins resolve before base class.\n All consumers continue to use: from src.core.superset_client import SupersetClient\n \"\"\"\n pass\n\n\n# #endregion SupersetClient\n\n# #endregion SupersetClientModule\n" + "body": "# #region SupersetClientModule [C:5] [TYPE Module] [SEMANTICS superset, package, superset-client]\n# @LAYER: Service\n# @BRIEF Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию.\n# @RELATION DEPENDS_ON -> [ConfigModels]\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION DEPENDS_ON -> [get_filename_from_headers]\n# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewFiltersMixin]\n#\n# @INVARIANT: All network operations must use the internal APIClient instance.\n# @PUBLIC_API: SupersetClient\n#\n# @RATIONALE: Decomposed from monolithic superset_client.py (2145 lines) into\n# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class\n# preserves the original public API surface — all consumers continue to import\n# `from src.core.superset_client import SupersetClient` without changes.\n# @REJECTED: Keeping a single 2145-line file — violates fractal limit INV_7.\n# @PRE: Superset instance URL and credentials configured\n# @POST: SupersetClient class exported\n# @SIDE_EFFECT: Establishes HTTP connection pool to Superset\n# @DATA_CONTRACT: SupersetConfig -> SupersetClient\n\nfrom ._base import SupersetClientBase\nfrom ._charts import SupersetChartsMixin\nfrom ._dashboards_crud import SupersetDashboardsCrudMixin\nfrom ._dashboards_filters import SupersetDashboardsFiltersMixin\nfrom ._dashboards_list import SupersetDashboardsListMixin\nfrom ._databases import SupersetDatabasesMixin\nfrom ._datasets import SupersetDatasetsMixin\nfrom ._datasets_preview import SupersetDatasetsPreviewMixin\nfrom ._datasets_preview_filters import SupersetDatasetsPreviewFiltersMixin\nfrom ._user_projection import SupersetUserProjectionMixin\n\n\n# #region SupersetClient [C:3] [TYPE Class]\n# @BRIEF Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами.\n# @RELATION DEPENDS_ON -> [ConfigModels]\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION INHERITS -> [SupersetClientBase]\n# @RELATION INHERITS -> [SupersetUserProjectionMixin]\n# @RELATION INHERITS -> [SupersetDashboardsListMixin]\n# @RELATION INHERITS -> [SupersetDashboardsFiltersMixin]\n# @RELATION INHERITS -> [SupersetDashboardsCrudMixin]\n# @RELATION INHERITS -> [SupersetChartsMixin]\n# @RELATION INHERITS -> [SupersetDatasetsMixin]\n# @RELATION INHERITS -> [SupersetDatasetsPreviewMixin]\n# @RELATION INHERITS -> [SupersetDatasetsPreviewFiltersMixin]\n# @RELATION INHERITS -> [SupersetDatabasesMixin]\nclass SupersetClient(\n SupersetDatabasesMixin,\n SupersetDatasetsPreviewFiltersMixin,\n SupersetDatasetsPreviewMixin,\n SupersetDatasetsMixin,\n SupersetChartsMixin,\n SupersetDashboardsCrudMixin,\n SupersetDashboardsFiltersMixin,\n SupersetDashboardsListMixin,\n SupersetUserProjectionMixin,\n SupersetClientBase,\n):\n \"\"\"Composed Superset REST API client.\n\n MRO order ensures domain mixins resolve before base class.\n All consumers continue to use: from src.core.superset_client import SupersetClient\n \"\"\"\n pass\n\n\n# #endregion SupersetClient\n\n# #endregion SupersetClientModule\n" }, { "contract_id": "SupersetClient", "contract_type": "Class", "file_path": "backend/src/core/superset_client/__init__.py", - "start_line": 31, - "end_line": 66, + "start_line": 35, + "end_line": 70, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -35848,14 +35352,16 @@ "contract_type": "Module", "file_path": "backend/src/core/superset_profile_lookup.py", "start_line": 1, - "end_line": 208, - "tier": "TIER_2", - "complexity": 3, + "end_line": 210, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ProfileQuery -> SupersetProfile", "INVARIANT": "Adapter never leaks raw upstream payload shape to API consumers.", - "LAYER": "Core", - "PURPOSE": "Provides environment-scoped Superset account lookup adapter with stable normalized output." + "LAYER": "Service", + "PURPOSE": "Provides environment-scoped Superset account lookup adapter with stable normalized output.", + "SIDE_EFFECT": "Makes HTTP requests to Superset" }, "relations": [ { @@ -35879,25 +35385,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SupersetProfileLookup [C:3] [TYPE Module] [SEMANTICS superset, profile, account, lookup, adapter]\n#\n# @BRIEF Provides environment-scoped Superset account lookup adapter with stable normalized output.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n#\n# @INVARIANT: Adapter never leaks raw upstream payload shape to API consumers.\nimport json\nfrom typing import Any\n\nfrom .logger import belief_scope, logger\nfrom .utils.network import APIClient, AuthenticationError, SupersetAPIError\n\n\n# #region SupersetAccountLookupAdapter [C:3] [TYPE Class]\n# @BRIEF Lookup Superset users and normalize candidates for profile binding.\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetProfileLookup]\nclass SupersetAccountLookupAdapter:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes lookup adapter with authenticated API client and environment context.\n # @PRE: network_client supports request(method, endpoint, params=...).\n # @POST: Adapter is ready to perform users lookup requests.\n def __init__(self, network_client: APIClient, environment_id: str):\n self.network_client = network_client\n self.environment_id = str(environment_id or \"\")\n # #endregion __init__\n # #region get_users_page [TYPE Function]\n # @PURPOSE: Fetch one users page from Superset with passthrough search/sort parameters.\n # @PRE: page_index >= 0 and page_size >= 1.\n # @POST: Returns deterministic payload with normalized items and total count.\n # @RETURN: Dict[str, Any]\n def get_users_page(\n self,\n search: str | None = None,\n page_index: int = 0,\n page_size: int = 20,\n sort_column: str = \"username\",\n sort_order: str = \"desc\",\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetAccountLookupAdapter.get_users_page\"):\n normalized_page_index = max(int(page_index), 0)\n normalized_page_size = max(int(page_size), 1)\n normalized_sort_column = str(sort_column or \"username\").strip().lower() or \"username\"\n normalized_sort_order = str(sort_order or \"desc\").strip().lower()\n if normalized_sort_order not in {\"asc\", \"desc\"}:\n normalized_sort_order = \"desc\"\n query: dict[str, Any] = {\n \"page\": normalized_page_index,\n \"page_size\": normalized_page_size,\n \"order_column\": normalized_sort_column,\n \"order_direction\": normalized_sort_order,\n }\n normalized_search = str(search or \"\").strip()\n if normalized_search:\n query[\"filters\"] = [{\"col\": \"username\", \"opr\": \"ct\", \"value\": normalized_search}]\n logger.reason(\n \"Lookup Superset users\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"page\": normalized_page_index, \"page_size\": normalized_page_size}},\n )\n logger.reflect(\n \"Prepared Superset users lookup query\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"order_column\": normalized_sort_column, \"sort_order\": normalized_sort_order, \"direction\": query.get(\"order_direction\")}},\n )\n primary_error: Exception | None = None\n last_error: Exception | None = None\n for attempt_index, endpoint in enumerate((\"/security/users/\", \"/security/users\"), start=1):\n try:\n logger.reason(\n \"Users lookup request attempt\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"attempt\": attempt_index, \"endpoint\": endpoint}},\n )\n response = self.network_client.request(\n method=\"GET\",\n endpoint=endpoint,\n params={\"q\": json.dumps(query)},\n )\n logger.reflect(\n \"Users lookup endpoint succeeded\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"attempt\": attempt_index, \"endpoint\": endpoint}},\n )\n return self._normalize_lookup_payload(\n response=response,\n page_index=normalized_page_index,\n page_size=normalized_page_size,\n )\n except Exception as exc:\n if primary_error is None:\n primary_error = exc\n last_error = exc\n cause = getattr(exc, \"__cause__\", None)\n cause_response = getattr(cause, \"response\", None)\n status_code = getattr(cause_response, \"status_code\", None)\n logger.explore(\n \"Users lookup endpoint failed\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"attempt\": attempt_index, \"endpoint\": endpoint, \"error_type\": type(exc).__name__, \"status_code\": status_code, \"direction\": query.get(\"order_direction\")}, \"error\": str(exc)},\n )\n if last_error is not None:\n selected_error: Exception = last_error\n if (\n primary_error is not None\n and primary_error is not last_error\n and isinstance(last_error, AuthenticationError)\n and not isinstance(primary_error, AuthenticationError)\n ):\n selected_error = primary_error\n logger.reflect(\n \"Preserving primary lookup failure over fallback auth error\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"primary_error_type\": type(primary_error).__name__, \"fallback_error_type\": type(last_error).__name__}},\n )\n logger.explore(\n \"All Superset users lookup endpoints failed\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"direction\": query.get(\"order_direction\"), \"selected_error_type\": type(selected_error).__name__}},\n )\n raise selected_error\n raise SupersetAPIError(\"Superset users lookup failed without explicit error\")\n # #endregion get_users_page\n # #region _normalize_lookup_payload [TYPE Function]\n # @PURPOSE: Convert Superset users response variants into stable candidates payload.\n # @PRE: response can be dict/list in any supported upstream shape.\n # @POST: Output contains canonical keys: status, environment_id, page_index, page_size, total, items.\n # @RETURN: Dict[str, Any]\n def _normalize_lookup_payload(\n self,\n response: Any,\n page_index: int,\n page_size: int,\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetAccountLookupAdapter._normalize_lookup_payload\"):\n payload = response\n if isinstance(payload, dict) and isinstance(payload.get(\"result\"), dict):\n payload = payload.get(\"result\")\n raw_items: list[Any] = []\n total = 0\n if isinstance(payload, dict):\n if isinstance(payload.get(\"result\"), list):\n raw_items = payload.get(\"result\") or []\n total = int(payload.get(\"count\", len(raw_items)) or 0)\n elif isinstance(payload.get(\"users\"), list):\n raw_items = payload.get(\"users\") or []\n total = int(payload.get(\"total\", len(raw_items)) or 0)\n elif isinstance(payload.get(\"items\"), list):\n raw_items = payload.get(\"items\") or []\n total = int(payload.get(\"total\", len(raw_items)) or 0)\n elif isinstance(payload, list):\n raw_items = payload\n total = len(raw_items)\n normalized_items: list[dict[str, Any]] = []\n seen_usernames = set()\n for raw_user in raw_items:\n candidate = self.normalize_user_payload(raw_user)\n username_key = str(candidate.get(\"username\") or \"\").strip().lower()\n if not username_key:\n continue\n if username_key in seen_usernames:\n continue\n seen_usernames.add(username_key)\n normalized_items.append(candidate)\n logger.reflect(\n \"Normalized lookup payload\",\n extra={\"src\": \"SupersetAccountLookupAdapter._normalize_lookup_payload\", \"payload\": {\"env\": self.environment_id, \"items\": len(normalized_items), \"total\": max(total, len(normalized_items))}},\n )\n return {\n \"status\": \"success\",\n \"environment_id\": self.environment_id,\n \"page_index\": max(int(page_index), 0),\n \"page_size\": max(int(page_size), 1),\n \"total\": max(int(total), len(normalized_items)),\n \"items\": normalized_items,\n }\n # #endregion _normalize_lookup_payload\n # #region normalize_user_payload [TYPE Function]\n # @PURPOSE: Project raw Superset user object to canonical candidate shape.\n # @PRE: raw_user may have heterogenous key names between Superset versions.\n # @POST: Returns normalized candidate keys (environment_id, username, display_name, email, is_active).\n # @RETURN: Dict[str, Any]\n def normalize_user_payload(self, raw_user: Any) -> dict[str, Any]:\n if not isinstance(raw_user, dict):\n raw_user = {}\n username = str(\n raw_user.get(\"username\")\n or raw_user.get(\"userName\")\n or raw_user.get(\"name\")\n or \"\"\n ).strip()\n full_name = str(raw_user.get(\"full_name\") or \"\").strip()\n first_name = str(raw_user.get(\"first_name\") or \"\").strip()\n last_name = str(raw_user.get(\"last_name\") or \"\").strip()\n display_name = full_name or \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if not display_name:\n display_name = username or None\n email = str(raw_user.get(\"email\") or \"\").strip() or None\n is_active_raw = raw_user.get(\"is_active\")\n is_active = bool(is_active_raw) if is_active_raw is not None else None\n return {\n \"environment_id\": self.environment_id,\n \"username\": username,\n \"display_name\": display_name,\n \"email\": email,\n \"is_active\": is_active,\n }\n # #endregion normalize_user_payload\n# #endregion SupersetAccountLookupAdapter\n# #endregion SupersetProfileLookup\n" + "body": "# #region SupersetProfileLookup [C:5] [TYPE Module] [SEMANTICS superset, profile, account, lookup, adapter]\n#\n# @BRIEF Provides environment-scoped Superset account lookup adapter with stable normalized output.\n# @LAYER: Service\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n#\n# @INVARIANT: Adapter never leaks raw upstream payload shape to API consumers.\n# @SIDE_EFFECT: Makes HTTP requests to Superset\n# @DATA_CONTRACT: ProfileQuery -> SupersetProfile\nimport json\nfrom typing import Any\n\nfrom .logger import belief_scope, logger\nfrom .utils.network import APIClient, AuthenticationError, SupersetAPIError\n\n\n# #region SupersetAccountLookupAdapter [C:3] [TYPE Class]\n# @BRIEF Lookup Superset users and normalize candidates for profile binding.\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetProfileLookup]\nclass SupersetAccountLookupAdapter:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes lookup adapter with authenticated API client and environment context.\n # @PRE: network_client supports request(method, endpoint, params=...).\n # @POST: Adapter is ready to perform users lookup requests.\n def __init__(self, network_client: APIClient, environment_id: str):\n self.network_client = network_client\n self.environment_id = str(environment_id or \"\")\n # #endregion __init__\n # #region get_users_page [TYPE Function]\n # @PURPOSE: Fetch one users page from Superset with passthrough search/sort parameters.\n # @PRE: page_index >= 0 and page_size >= 1.\n # @POST: Returns deterministic payload with normalized items and total count.\n # @RETURN: Dict[str, Any]\n def get_users_page(\n self,\n search: str | None = None,\n page_index: int = 0,\n page_size: int = 20,\n sort_column: str = \"username\",\n sort_order: str = \"desc\",\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetAccountLookupAdapter.get_users_page\"):\n normalized_page_index = max(int(page_index), 0)\n normalized_page_size = max(int(page_size), 1)\n normalized_sort_column = str(sort_column or \"username\").strip().lower() or \"username\"\n normalized_sort_order = str(sort_order or \"desc\").strip().lower()\n if normalized_sort_order not in {\"asc\", \"desc\"}:\n normalized_sort_order = \"desc\"\n query: dict[str, Any] = {\n \"page\": normalized_page_index,\n \"page_size\": normalized_page_size,\n \"order_column\": normalized_sort_column,\n \"order_direction\": normalized_sort_order,\n }\n normalized_search = str(search or \"\").strip()\n if normalized_search:\n query[\"filters\"] = [{\"col\": \"username\", \"opr\": \"ct\", \"value\": normalized_search}]\n logger.reason(\n \"Lookup Superset users\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"page\": normalized_page_index, \"page_size\": normalized_page_size}},\n )\n logger.reflect(\n \"Prepared Superset users lookup query\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"order_column\": normalized_sort_column, \"sort_order\": normalized_sort_order, \"direction\": query.get(\"order_direction\")}},\n )\n primary_error: Exception | None = None\n last_error: Exception | None = None\n for attempt_index, endpoint in enumerate((\"/security/users/\", \"/security/users\"), start=1):\n try:\n logger.reason(\n \"Users lookup request attempt\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"attempt\": attempt_index, \"endpoint\": endpoint}},\n )\n response = self.network_client.request(\n method=\"GET\",\n endpoint=endpoint,\n params={\"q\": json.dumps(query)},\n )\n logger.reflect(\n \"Users lookup endpoint succeeded\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"attempt\": attempt_index, \"endpoint\": endpoint}},\n )\n return self._normalize_lookup_payload(\n response=response,\n page_index=normalized_page_index,\n page_size=normalized_page_size,\n )\n except Exception as exc:\n if primary_error is None:\n primary_error = exc\n last_error = exc\n cause = getattr(exc, \"__cause__\", None)\n cause_response = getattr(cause, \"response\", None)\n status_code = getattr(cause_response, \"status_code\", None)\n logger.explore(\n \"Users lookup endpoint failed\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"attempt\": attempt_index, \"endpoint\": endpoint, \"error_type\": type(exc).__name__, \"status_code\": status_code, \"direction\": query.get(\"order_direction\")}, \"error\": str(exc)},\n )\n if last_error is not None:\n selected_error: Exception = last_error\n if (\n primary_error is not None\n and primary_error is not last_error\n and isinstance(last_error, AuthenticationError)\n and not isinstance(primary_error, AuthenticationError)\n ):\n selected_error = primary_error\n logger.reflect(\n \"Preserving primary lookup failure over fallback auth error\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"primary_error_type\": type(primary_error).__name__, \"fallback_error_type\": type(last_error).__name__}},\n )\n logger.explore(\n \"All Superset users lookup endpoints failed\",\n extra={\"src\": \"SupersetAccountLookupAdapter.get_users_page\", \"payload\": {\"env\": self.environment_id, \"direction\": query.get(\"order_direction\"), \"selected_error_type\": type(selected_error).__name__}},\n )\n raise selected_error\n raise SupersetAPIError(\"Superset users lookup failed without explicit error\")\n # #endregion get_users_page\n # #region _normalize_lookup_payload [TYPE Function]\n # @PURPOSE: Convert Superset users response variants into stable candidates payload.\n # @PRE: response can be dict/list in any supported upstream shape.\n # @POST: Output contains canonical keys: status, environment_id, page_index, page_size, total, items.\n # @RETURN: Dict[str, Any]\n def _normalize_lookup_payload(\n self,\n response: Any,\n page_index: int,\n page_size: int,\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetAccountLookupAdapter._normalize_lookup_payload\"):\n payload = response\n if isinstance(payload, dict) and isinstance(payload.get(\"result\"), dict):\n payload = payload.get(\"result\")\n raw_items: list[Any] = []\n total = 0\n if isinstance(payload, dict):\n if isinstance(payload.get(\"result\"), list):\n raw_items = payload.get(\"result\") or []\n total = int(payload.get(\"count\", len(raw_items)) or 0)\n elif isinstance(payload.get(\"users\"), list):\n raw_items = payload.get(\"users\") or []\n total = int(payload.get(\"total\", len(raw_items)) or 0)\n elif isinstance(payload.get(\"items\"), list):\n raw_items = payload.get(\"items\") or []\n total = int(payload.get(\"total\", len(raw_items)) or 0)\n elif isinstance(payload, list):\n raw_items = payload\n total = len(raw_items)\n normalized_items: list[dict[str, Any]] = []\n seen_usernames = set()\n for raw_user in raw_items:\n candidate = self.normalize_user_payload(raw_user)\n username_key = str(candidate.get(\"username\") or \"\").strip().lower()\n if not username_key:\n continue\n if username_key in seen_usernames:\n continue\n seen_usernames.add(username_key)\n normalized_items.append(candidate)\n logger.reflect(\n \"Normalized lookup payload\",\n extra={\"src\": \"SupersetAccountLookupAdapter._normalize_lookup_payload\", \"payload\": {\"env\": self.environment_id, \"items\": len(normalized_items), \"total\": max(total, len(normalized_items))}},\n )\n return {\n \"status\": \"success\",\n \"environment_id\": self.environment_id,\n \"page_index\": max(int(page_index), 0),\n \"page_size\": max(int(page_size), 1),\n \"total\": max(int(total), len(normalized_items)),\n \"items\": normalized_items,\n }\n # #endregion _normalize_lookup_payload\n # #region normalize_user_payload [TYPE Function]\n # @PURPOSE: Project raw Superset user object to canonical candidate shape.\n # @PRE: raw_user may have heterogenous key names between Superset versions.\n # @POST: Returns normalized candidate keys (environment_id, username, display_name, email, is_active).\n # @RETURN: Dict[str, Any]\n def normalize_user_payload(self, raw_user: Any) -> dict[str, Any]:\n if not isinstance(raw_user, dict):\n raw_user = {}\n username = str(\n raw_user.get(\"username\")\n or raw_user.get(\"userName\")\n or raw_user.get(\"name\")\n or \"\"\n ).strip()\n full_name = str(raw_user.get(\"full_name\") or \"\").strip()\n first_name = str(raw_user.get(\"first_name\") or \"\").strip()\n last_name = str(raw_user.get(\"last_name\") or \"\").strip()\n display_name = full_name or \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if not display_name:\n display_name = username or None\n email = str(raw_user.get(\"email\") or \"\").strip() or None\n is_active_raw = raw_user.get(\"is_active\")\n is_active = bool(is_active_raw) if is_active_raw is not None else None\n return {\n \"environment_id\": self.environment_id,\n \"username\": username,\n \"display_name\": display_name,\n \"email\": email,\n \"is_active\": is_active,\n }\n # #endregion normalize_user_payload\n# #endregion SupersetAccountLookupAdapter\n# #endregion SupersetProfileLookup\n" }, { "contract_id": "SupersetAccountLookupAdapter", "contract_type": "Class", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 17, - "end_line": 207, + "start_line": 19, + "end_line": 209, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -35927,8 +35442,8 @@ "contract_id": "__init__", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 22, - "end_line": 29, + "start_line": 24, + "end_line": 31, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -35965,8 +35480,8 @@ "contract_id": "get_users_page", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 30, - "end_line": 119, + "start_line": 32, + "end_line": 121, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36010,8 +35525,8 @@ "contract_id": "_normalize_lookup_payload", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 120, - "end_line": 173, + "start_line": 122, + "end_line": 175, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36055,8 +35570,8 @@ "contract_id": "normalize_user_payload", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 174, - "end_line": 206, + "start_line": 176, + "end_line": 208, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36955,85 +36470,68 @@ "body": " # #region create_sub_context [TYPE Function]\n # @PURPOSE: Create a sub-context with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskContext with different logger source.\n # @PARAM: source (str) - New default source for logging.\n # @RETURN: TaskContext - New context with different source.\n def create_sub_context(self, source: str) -> \"TaskContext\":\n \"\"\"Create a sub-context with a different default source for logging.\"\"\"\n with belief_scope(\"create_sub_context\"):\n return TaskContext(\n task_id=self._task_id,\n add_log_fn=self._logger._add_log,\n params=self._params,\n default_source=source,\n background_tasks=self._background_tasks,\n )\n # #endregion create_sub_context\n" }, { - "contract_id": "TaskManagerModule", + "contract_id": "EventBusModule", "contract_type": "Module", - "file_path": "backend/src/core/task_manager/manager.py", + "file_path": "backend/src/core/task_manager/event_bus.py", "start_line": 1, - "end_line": 708, + "end_line": 229, "tier": "TIER_3", "complexity": 5, "metadata": { "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[plugin_id, params] -> Model[Task, LogEntry]", - "INVARIANT": "Task IDs are unique.", - "LAYER": "Core", - "POST": "Orchestrates task execution and persistence.", - "PRE": "Plugin loader and database sessions are initialized.", - "PURPOSE": "Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously.", - "SIDE_EFFECT": "Spawns worker threads and flushes logs to DB.", - "TEST_CONTRACT": "TaskManagerRuntime -> {" + "PURPOSE": "Task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time" }, - "relations": [ - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "PluginLoader", - "target_ref": "[PluginLoader]" - }, - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "TaskPersistenceService", - "target_ref": "[TaskPersistenceService]" - }, - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "TaskLogPersistenceService", - "target_ref": "[TaskLogPersistenceService]" - }, - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "TaskContext", - "target_ref": "[TaskContext]" - }, - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - }, - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - }, - { - "source_id": "TaskManagerModule", - "relation_type": "DEPENDS_ON", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], + "relations": [], "schema_warnings": [ { - "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] + "actual_complexity": 5, + "contract_type": "Module" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C5", + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", "detail": { "actual_complexity": 5, "contract_type": "Module" @@ -37042,25 +36540,1237 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskManagerModule [C:5] [TYPE Module] [SEMANTICS task, schedule, execution, task-manager]\n# @BRIEF Manages the lifecycle of tasks, including their creation, execution, and state tracking. It uses a thread pool to run plugins asynchronously.\n# @LAYER: Core\n# @PRE: Plugin loader and database sessions are initialized.\n# @POST: Orchestrates task execution and persistence.\n# @SIDE_EFFECT: Spawns worker threads and flushes logs to DB.\n# @DATA_CONTRACT: Input[plugin_id, params] -> Model[Task, LogEntry]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT: Task IDs are unique.\n# @TEST_CONTRACT: TaskManagerRuntime -> {\n# required_fields: {plugin_loader: PluginLoader},\n# optional_fields: {},\n# invariants: [\"Must use belief_scope for logging\"]\n# }\n# @TEST_FIXTURE: valid_module -> {\"manager_initialized\": true}\n# @TEST_EDGE: missing_required_field -> {\"plugin_loader\": null}\n# @TEST_EDGE: empty_response -> {\"tasks\": []}\n# @TEST_EDGE: invalid_type -> {\"plugin_loader\": \"string_instead_of_object\"}\n# @TEST_EDGE: external_failure -> {\"db_unavailable\": true}\n# @TEST_INVARIANT: logger_compliance -> verifies: [valid_module]\nimport asyncio\nimport inspect\nimport threading\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ..cot_logger import seed_trace_id\nfrom ..logger import belief_scope, logger, should_log_task_level\nfrom .context import TaskContext\nfrom .models import LogEntry, LogFilter, LogStats, Task, TaskStatus\nfrom .persistence import TaskLogPersistenceService, TaskPersistenceService\n\n\n# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state]\n# @BRIEF Manages the lifecycle of tasks, including their creation, execution, and state tracking.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @PRE: Plugin loader resolves plugin ids and persistence services are available for task state hydration.\n# @POST: In-memory task graph, lifecycle scheduler, and log event bus stay consistent with persisted task state.\n# @INVARIANT: Task IDs are unique within the registry.\n# @INVARIANT: Each task has exactly one status at any time.\n# @INVARIANT: Log entries are never deleted after being added to a task.\n# @SIDE_EFFECT: Spawns worker threads, flushes logs to database, and mutates task states.\n# @DATA_CONTRACT: Input[plugin_id, params] -> Output[Task]\nclass TaskManager:\n \"\"\"\n Manages the lifecycle of tasks, including their creation, execution, and state tracking.\n \"\"\"\n # Log flush interval in seconds\n LOG_FLUSH_INTERVAL = 2.0\n # #region TaskGraph [TYPE Block] [C:5]\n # @PURPOSE: Represents the in-memory task dependency graph spanning task registry nodes, paused futures, and persistence-backed hydration.\n # @RELATION: [DEPENDS_ON] ->[Task]\n # @RELATION: [DEPENDS_ON] ->[TaskPersistenceService]\n # @PRE: Task ids are generated before insertion and persisted tasks can be reconstructed into Task models.\n # @POST: Each live task id resolves to at most one active Task node and optional pause future.\n # @SIDE_EFFECT: Mutates the in-memory task registry and loads persisted state during manager startup.\n # @DATA_CONTRACT: Input[Task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]]\n # @INVARIANT: Registry membership is keyed by unique task id and survives log streaming side channels.\n # #endregion TaskGraph\n # #region EventBus [TYPE Block] [C:5]\n # @PURPOSE: Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time observers.\n # @RELATION: [DEPENDS_ON] ->[LogEntry]\n # @RELATION: [DEPENDS_ON] ->[TaskLogPersistenceService]\n # @PRE: Task log records accept structured LogEntry payloads and subscribers consume asyncio queues.\n # @POST: Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers in emission order.\n # @SIDE_EFFECT: Writes task logs to persistence, mutates in-memory buffers, and pushes log entries into subscriber queues.\n # @DATA_CONTRACT: Input[task_id, LogEntry, Queue subscribers] -> Output[persisted log rows, streamed log events]\n # @INVARIANT: Buffered logs are retried on persistence failure and every subscriber receives only task-scoped events.\n # #endregion EventBus\n # #region JobLifecycle [TYPE Block] [C:5]\n # @PURPOSE: Encodes task creation, execution, pause/resume, and completion transitions for plugin-backed jobs.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n # @RELATION: [DEPENDS_ON] ->[TaskContext]\n # @RELATION: [DEPENDS_ON] ->[PluginLoader]\n # @PRE: Requested plugin ids resolve to executable plugins and task state transitions target known TaskStatus values.\n # @POST: Every scheduled task transitions through a valid lifecycle path ending in persisted terminal or waiting state.\n # @SIDE_EFFECT: Schedules async execution, pauses on input/mapping requests, and mutates persisted task lifecycle markers.\n # @DATA_CONTRACT: Input[plugin_id, params, task_id, resolution payloads] -> Output[Task status transitions, task result]\n # @INVARIANT: A task cannot be resumed from a waiting state unless a matching future exists or a new wait future is created.\n # #endregion JobLifecycle\n # #region __init__ [TYPE Function] [C:5]\n # @PURPOSE: Initialize the TaskManager with dependencies.\n # @PRE: plugin_loader is initialized.\n # @POST: TaskManager is ready to accept tasks.\n # @SIDE_EFFECT: Starts background flusher thread and loads persisted task state into memory.\n # @RELATION: [CALLS] ->[TaskPersistenceService.load_tasks]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n # @PARAM: plugin_loader - The plugin loader instance.\n def __init__(self, plugin_loader):\n with belief_scope(\"TaskManager.__init__\"):\n logger.reason(\"Initializing task manager runtime services\")\n self.plugin_loader = plugin_loader\n self.tasks: dict[str, Task] = {}\n self.subscribers: dict[str, list[asyncio.Queue]] = {}\n self.executor = ThreadPoolExecutor(\n max_workers=5\n ) # For CPU-bound plugin execution\n self.persistence_service = TaskPersistenceService()\n self.log_persistence_service = TaskLogPersistenceService()\n # Log buffer: task_id -> List[LogEntry]\n self._log_buffer: dict[str, list[LogEntry]] = {}\n self._log_buffer_lock = threading.Lock()\n # Flusher thread for batch writing logs\n self._flusher_stop_event = threading.Event()\n self._flusher_thread = threading.Thread(\n target=self._flusher_loop, daemon=True\n )\n self._flusher_thread.start()\n try:\n self.loop = asyncio.get_running_loop()\n except RuntimeError:\n self.loop = asyncio.get_event_loop()\n self.task_futures: dict[str, asyncio.Future] = {}\n # Load persisted tasks on startup\n self.load_persisted_tasks()\n logger.reflect(\n \"Task manager runtime initialized\",\n extra={\"task_count\": len(self.tasks)},\n )\n # #endregion __init__\n # #region _flusher_loop [TYPE Function] [C:3]\n # @PURPOSE: Background thread that periodically flushes log buffer to database.\n # @PRE: TaskManager is initialized.\n # @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.\n # @RELATION: [CALLS] ->[TaskManager._flush_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def _flusher_loop(self):\n \"\"\"Background thread that flushes log buffer to database.\"\"\"\n seed_trace_id() # seed trace_id for the daemon flusher thread\n while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n # #endregion _flusher_loop\n # #region _flush_logs [TYPE Function] [C:3]\n # @PURPOSE: Flush all buffered logs to the database.\n # @PRE: None.\n # @POST: All buffered logs are written to task_logs table.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.add_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def _flush_logs(self):\n \"\"\"Flush all buffered logs to the database.\"\"\"\n seed_trace_id()\n with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n logger.debug(f\"Flushed {len(logs)} logs for task {task_id}\")\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # Re-add logs to buffer on failure\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].extend(logs)\n # #endregion _flush_logs\n # #region _flush_task_logs [TYPE Function] [C:3]\n # @PURPOSE: Flush logs for a specific task immediately.\n # @PRE: task_id exists.\n # @POST: Task's buffered logs are written to database.\n # @PARAM: task_id (str) - The task ID.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.add_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def _flush_task_logs(self, task_id: str):\n \"\"\"Flush logs for a specific task immediately.\"\"\"\n with belief_scope(\"_flush_task_logs\"):\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # #endregion _flush_task_logs\n # #region create_task [TYPE Function] [C:3]\n # @PURPOSE: Creates and queues a new task for execution.\n # @PRE: Plugin with plugin_id exists. Params are valid.\n # @POST: Task is created, added to registry, and scheduled for execution.\n # @PARAM: plugin_id (str) - The ID of the plugin to run.\n # @PARAM: params (Dict[str, Any]) - Parameters for the plugin.\n # @PARAM: user_id (Optional[str]) - ID of the user requesting the task.\n # @RETURN: Task - The created task instance.\n # @THROWS: ValueError if plugin not found or params invalid.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None\n ) -> Task:\n with belief_scope(\"TaskManager.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.tasks[task.id] = task\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id)\n ) # Schedule task for execution\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n # #region _run_task [TYPE Function] [C:3]\n # @PURPOSE: Internal method to execute a task with TaskContext support.\n # @PRE: Task exists in registry.\n # @POST: Task is executed, status updated to SUCCESS or FAILED.\n # @PARAM: task_id (str) - The ID of the task to run.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n # @RELATION: [DEPENDS_ON] ->[TaskContext]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n async def _run_task(self, task_id: str):\n seed_trace_id()\n with belief_scope(\"TaskManager._run_task\", f\"task_id={task_id}\"):\n task = self.tasks[task_id]\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n self._add_log(\n task_id,\n \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n # Prepare params and check if plugin supports new TaskContext\n params = {**task.params, \"_task_id\": task_id}\n # Check if plugin's execute method accepts 'context' parameter\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=self._add_log,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n # Backward compatibility: old-style plugins without context\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n self._add_log(\n task_id,\n \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n self._add_log(\n task_id,\n \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n # Flush any remaining buffered logs before persisting task\n self._flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # #endregion _run_task\n # #region resolve_task [TYPE Function] [C:3]\n # @PURPOSE: Resumes a task that is awaiting mapping.\n # @PRE: Task exists and is in AWAITING_MAPPING state.\n # @POST: Task status updated to RUNNING, params updated, execution resumed.\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: resolution_params (Dict[str, Any]) - Params to resolve the wait.\n # @THROWS: ValueError if task not found or not awaiting mapping.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n async def resolve_task(self, task_id: str, resolution_params: dict[str, Any]):\n with belief_scope(\"TaskManager.resolve_task\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n # Update task params with resolution\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self._add_log(task_id, \"INFO\", \"Task resumed after mapping resolution.\")\n # Signal the future to continue\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n # #endregion resolve_task\n # #region wait_for_resolution [TYPE Function] [C:3]\n # @PURPOSE: Pauses execution and waits for a resolution signal.\n # @PRE: Task exists.\n # @POST: Execution pauses until future is set.\n # @PARAM: task_id (str) - The ID of the task.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n async def wait_for_resolution(self, task_id: str):\n with belief_scope(\"TaskManager.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n self.task_futures[task_id] = self.loop.create_future()\n try:\n await self.task_futures[task_id]\n finally:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion wait_for_resolution\n # #region wait_for_input [TYPE Function] [C:3]\n # @PURPOSE: Pauses execution and waits for user input.\n # @PRE: Task exists.\n # @POST: Execution pauses until future is set via resume_task_with_password.\n # @PARAM: task_id (str) - The ID of the task.\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n async def wait_for_input(self, task_id: str):\n with belief_scope(\"TaskManager.wait_for_input\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n return\n # Status is already set to AWAITING_INPUT by await_input()\n self.task_futures[task_id] = self.loop.create_future()\n try:\n await self.task_futures[task_id]\n finally:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion wait_for_input\n # #region get_task [TYPE Function] [C:3]\n # @PURPOSE: Retrieves a task by its ID.\n # @PRE: task_id is a string.\n # @POST: Returns Task object or None.\n # @PARAM: task_id (str) - ID of the task.\n # @RETURN: Optional[Task] - The task or None.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def get_task(self, task_id: str) -> Task | None:\n with belief_scope(\"TaskManager.get_task\", f\"task_id={task_id}\"):\n return self.tasks.get(task_id)\n # #endregion get_task\n # #region get_all_tasks [TYPE Function] [C:3]\n # @PURPOSE: Retrieves all registered tasks.\n # @PRE: None.\n # @POST: Returns list of all Task objects.\n # @RETURN: List[Task] - All tasks.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def get_all_tasks(self) -> list[Task]:\n with belief_scope(\"TaskManager.get_all_tasks\"):\n return list(self.tasks.values())\n # #endregion get_all_tasks\n # #region get_tasks [TYPE Function] [C:3]\n # @PURPOSE: Retrieves tasks with pagination and optional status filter.\n # @PRE: limit and offset are non-negative integers.\n # @POST: Returns a list of tasks sorted by start_time descending.\n # @PARAM: limit (int) - Maximum number of tasks to return.\n # @PARAM: offset (int) - Number of tasks to skip.\n # @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n # @RETURN: List[Task] - List of tasks matching criteria.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n with belief_scope(\"TaskManager.get_tasks\"):\n tasks = list(self.tasks.values())\n if status:\n tasks = [t for t in tasks if t.status == status]\n if plugin_ids:\n plugin_id_set = set(plugin_ids)\n tasks = [t for t in tasks if t.plugin_id in plugin_id_set]\n if completed_only:\n tasks = [\n t for t in tasks if t.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]\n ]\n # Sort by started_at descending with tolerant handling of mixed tz-aware/naive values.\n def sort_key(task: Task) -> float:\n started_at = task.started_at\n if started_at is None:\n return float(\"-inf\")\n if not isinstance(started_at, datetime):\n return float(\"-inf\")\n if started_at.tzinfo is None:\n return started_at.replace(tzinfo=UTC).timestamp()\n return started_at.timestamp()\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n # #endregion get_tasks\n # #region get_task_logs [TYPE Function] [C:3]\n # @PURPOSE: Retrieves logs for a specific task (from memory for running, persistence for completed).\n # @PRE: task_id is a string.\n # @POST: Returns list of LogEntry or TaskLog objects.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: log_filter (Optional[LogFilter]) - Filter parameters.\n # @RETURN: List[LogEntry] - List of log entries.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None\n ) -> list[LogEntry]:\n with belief_scope(\"TaskManager.get_task_logs\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n # For completed tasks, fetch from persistence\n if task and task.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]:\n if log_filter is None:\n log_filter = LogFilter()\n task_logs = self.log_persistence_service.get_logs(task_id, log_filter)\n # Convert TaskLog to LogEntry for backward compatibility\n return [\n LogEntry(\n timestamp=log.timestamp,\n level=log.level,\n message=log.message,\n source=log.source,\n metadata=log.metadata,\n )\n for log in task_logs\n ]\n # For running/pending tasks, return from memory\n return task.logs if task else []\n # #endregion get_task_logs\n # #region get_task_log_stats [TYPE Function] [C:3]\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_log_stats]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def get_task_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\"TaskManager.get_task_log_stats\", f\"task_id={task_id}\"):\n return self.log_persistence_service.get_log_stats(task_id)\n # #endregion get_task_log_stats\n # #region get_task_log_sources [TYPE Function] [C:3]\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_sources]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def get_task_log_sources(self, task_id: str) -> list[str]:\n with belief_scope(\"TaskManager.get_task_log_sources\", f\"task_id={task_id}\"):\n return self.log_persistence_service.get_sources(task_id)\n # #endregion get_task_log_sources\n # #region _add_log [TYPE Function] [C:3]\n # @PURPOSE: Adds a log entry to a task buffer and notifies subscribers.\n # @PRE: Task exists.\n # @POST: Log added to buffer and pushed to queues (if level meets task_log_level filter).\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: level (str) - Log level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (str) - Source component (default: \"system\").\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @PARAM: context (Optional[Dict]) - Legacy context (for backward compatibility).\n # @RELATION: [CALLS] ->[should_log_task_level]\n # @RELATION: [DISPATCHES] ->[EventBus]\n def _add_log(\n self,\n task_id: str,\n level: str,\n message: str,\n source: str = \"system\",\n metadata: dict[str, Any] | None = None,\n context: dict[str, Any] | None = None,\n ):\n with belief_scope(\"TaskManager._add_log\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n return\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n # Create log entry with new fields\n log_entry = LogEntry(\n level=level,\n message=message,\n source=source,\n metadata=metadata,\n context=context, # Keep for backward compatibility\n )\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n task.logs.append(log_entry)\n # Add to buffer for batch persistence\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].append(log_entry)\n # Notify subscribers (for real-time WebSocket updates)\n if task_id in self.subscribers:\n for queue in self.subscribers[task_id]:\n self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)\n # #endregion _add_log\n # #region subscribe_logs [TYPE Function] [C:3]\n # @PURPOSE: Subscribes to real-time logs for a task.\n # @PRE: task_id is a string.\n # @POST: Returns an asyncio.Queue for log entries.\n # @PARAM: task_id (str) - ID of the task.\n # @RETURN: asyncio.Queue - Queue for log entries.\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n with belief_scope(\"TaskManager.subscribe_logs\", f\"task_id={task_id}\"):\n queue = asyncio.Queue()\n if task_id not in self.subscribers:\n self.subscribers[task_id] = []\n self.subscribers[task_id].append(queue)\n return queue\n # #endregion subscribe_logs\n # #region unsubscribe_logs [TYPE Function] [C:3]\n # @PURPOSE: Unsubscribes from real-time logs for a task.\n # @PRE: task_id is a string, queue is asyncio.Queue.\n # @POST: Queue removed from subscribers.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: queue (asyncio.Queue) - Queue to remove.\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n with belief_scope(\"TaskManager.unsubscribe_logs\", f\"task_id={task_id}\"):\n if task_id in self.subscribers:\n if queue in self.subscribers[task_id]:\n self.subscribers[task_id].remove(queue)\n if not self.subscribers[task_id]:\n del self.subscribers[task_id]\n # #endregion unsubscribe_logs\n # #region load_persisted_tasks [TYPE Function] [C:3]\n # @PURPOSE: Load persisted tasks using persistence service.\n # @PRE: None.\n # @POST: Persisted tasks loaded into self.tasks.\n # @RELATION: [CALLS] ->[TaskPersistenceService.load_tasks]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def load_persisted_tasks(self) -> None:\n with belief_scope(\"TaskManager.load_persisted_tasks\"):\n loaded_tasks = self.persistence_service.load_tasks(limit=100)\n for task in loaded_tasks:\n if task.id not in self.tasks:\n self.tasks[task.id] = task\n # #endregion load_persisted_tasks\n # #region await_input [TYPE Function] [C:3]\n # @PURPOSE: Transition a task to AWAITING_INPUT state with input request.\n # @PRE: Task exists and is in RUNNING state.\n # @POST: Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: input_request (Dict) - Details about required input.\n # @THROWS: ValueError if task not found or not RUNNING.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n def await_input(self, task_id: str, input_request: dict[str, Any]) -> None:\n with belief_scope(\"TaskManager.await_input\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n self._add_log(\n task_id,\n \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n # #region resume_task_with_password [TYPE Function] [C:3]\n # @PURPOSE: Resume a task that is awaiting input with provided passwords.\n # @PRE: Task exists and is in AWAITING_INPUT state.\n # @POST: Task status changed to RUNNING, passwords injected, task resumed.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: passwords (Dict[str, str]) - Mapping of database name to password.\n # @THROWS: ValueError if task not found, not awaiting input, or passwords invalid.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str]\n ) -> None:\n with belief_scope(\n \"TaskManager.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.tasks.get(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self._add_log(\n task_id,\n \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n # #endregion resume_task_with_password\n # #region clear_tasks [TYPE Function] [C:3]\n # @PURPOSE: Clears tasks based on status filter (also deletes associated logs).\n # @PRE: status is Optional[TaskStatus].\n # @POST: Tasks matching filter (or all non-active) cleared from registry and database.\n # @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n # @RETURN: int - Number of tasks cleared.\n # @RELATION: [CALLS] ->[TaskPersistenceService.delete_tasks]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def clear_tasks(self, status: TaskStatus | None = None) -> int:\n with belief_scope(\"TaskManager.clear_tasks\"):\n tasks_to_remove = []\n for task_id, task in list(self.tasks.items()):\n # If status is provided, match it.\n # If status is None, match everything EXCEPT RUNNING (unless they are awaiting input/mapping which are technically running but paused?)\n # Actually, AWAITING_INPUT and AWAITING_MAPPING are distinct statuses in TaskStatus enum.\n # RUNNING is active execution.\n should_remove = False\n if status:\n if task.status == status:\n should_remove = True\n else:\n # Clear all non-active tasks (keep RUNNING, AWAITING_INPUT, AWAITING_MAPPING)\n if task.status not in [\n TaskStatus.RUNNING,\n TaskStatus.AWAITING_INPUT,\n TaskStatus.AWAITING_MAPPING,\n ]:\n should_remove = True\n if should_remove:\n tasks_to_remove.append(task_id)\n for tid in tasks_to_remove:\n # Cancel future if exists (e.g. for AWAITING_INPUT/MAPPING)\n if tid in self.task_futures:\n self.task_futures[tid].cancel()\n del self.task_futures[tid]\n del self.tasks[tid]\n # Remove from persistence (task_records and task_logs via CASCADE)\n self.persistence_service.delete_tasks(tasks_to_remove)\n # Also explicitly delete logs (in case CASCADE is not set up)\n if tasks_to_remove:\n self.log_persistence_service.delete_logs_for_tasks(tasks_to_remove)\n logger.info(f\"Cleared {len(tasks_to_remove)} tasks.\")\n return len(tasks_to_remove)\n # #endregion clear_tasks\n# #endregion TaskManager\n# #endregion TaskManagerModule\n" + "body": "# #region EventBusModule [C:5] [TYPE Module] [SEMANTICS event,bus,log,buffer,subscriber,flush]\n# @BRIEF Task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time\n# WebSocket observers — extracted from the monolithic TaskManager.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [should_log_task_level]\n# @RATIONALE Extracted from TaskManager to satisfy INV_7. EventBus owns log buffering, batch\n# persistence flushing, and pub/sub subscription management — a cohesive unit with\n# independent threading and I/O patterns.\n# @REJECTED Keeping log/event concerns inside TaskManager was rejected — the background flusher\n# thread, buffer locks, and subscriber queues are architecturally distinct from task\n# lifecycle state machines and the in-memory task registry.\n# @PRE TaskLogPersistenceService is initialized.\n# @POST Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers.\n# @SIDE_EFFECT Writes task logs to persistence, mutates in-memory buffers, pushes log entries\n# into subscriber queues, runs background flusher thread.\n# @INVARIANT Buffered logs are retried on persistence failure; every subscriber receives only\n# task-scoped events.\n# @DATA_CONTRACT Input: LogEntry -> Output: persisted log + subscriber notification\n\nimport asyncio\nimport threading\nfrom typing import Any\n\nfrom ..cot_logger import seed_trace_id\nfrom ..logger import belief_scope, logger, should_log_task_level\nfrom .models import LogEntry, LogFilter, LogStats\nfrom .persistence import TaskLogPersistenceService\n\n\n# #region EventBus [C:5] [TYPE Class] [SEMANTICS event,bus,log,buffer,subscriber,flush]\n# @BRIEF Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out\n# for real-time observers.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [should_log_task_level]\n# @PRE asyncio loop is available for thread-safe queue operations.\n# @POST Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers.\n# @INVARIANT Buffered logs are retried on persistence failure.\n# @RATIONALE Extracted as standalone pub/sub event bus to isolate threading (background flusher\n# thread, buffer locks) and subscription management from task lifecycle and registry.\n# @REJECTED Keeping log buffering inside TaskManager was rejected — the background flusher\n# thread and subscriber queues are architecturally distinct from task state machines\n# and should be independently stoppable/testable.\nclass EventBus:\n \"\"\"Task-scoped log buffering, persistence flushes, and subscriber fan-out.\"\"\"\n\n LOG_FLUSH_INTERVAL = 2.0\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize empty log buffer, subscriber registry, and start background flusher.\n def __init__(self, log_persistence_service: TaskLogPersistenceService, loop: asyncio.AbstractEventLoop):\n self.log_persistence_service = log_persistence_service\n self.loop = loop\n self.subscribers: dict[str, list[asyncio.Queue]] = {}\n self._log_buffer: dict[str, list[LogEntry]] = {}\n self._log_buffer_lock = threading.Lock()\n self._flusher_stop_event = threading.Event()\n self._flusher_thread = threading.Thread(\n target=self._flusher_loop, daemon=True\n )\n self._flusher_thread.start()\n # #endregion __init__\n\n # #region stop [TYPE Function]\n # @BRIEF Signal the flusher thread to stop and wait for it.\n def stop(self) -> None:\n self._flusher_stop_event.set()\n self._flusher_thread.join(timeout=2)\n # #endregion stop\n\n # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Background thread that periodically flushes log buffer to database.\n def _flusher_loop(self):\n seed_trace_id()\n while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n # #endregion _flusher_loop\n\n # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Flush all buffered logs to the database.\n # @RELATION CALLS -> [TaskLogPersistenceService.add_logs]\n def _flush_logs(self):\n seed_trace_id()\n with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n logger.debug(f\"Flushed {len(logs)} logs for task {task_id}\")\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].extend(logs)\n # #endregion _flush_logs\n\n # #region flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Flush logs for a specific task immediately.\n # @PRE task_id exists.\n # @POST Task's buffered logs are written to database.\n # @RELATION CALLS -> [TaskLogPersistenceService.add_logs]\n def flush_task_logs(self, task_id: str):\n with belief_scope(\"EventBus.flush_task_logs\"):\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # #endregion flush_task_logs\n\n # #region add_log [C:3] [TYPE Function] [SEMANTICS log,buffer,notify,subscriber]\n # @BRIEF Adds a log entry to a task buffer and notifies subscribers.\n # @PRE Task exists.\n # @POST Log added to buffer and pushed to queues (if level meets task_log_level filter).\n # @RELATION CALLS -> [should_log_task_level]\n def add_log(\n self,\n task_id: str,\n level: str,\n message: str,\n source: str = \"system\",\n metadata: dict[str, Any] | None = None,\n context: dict[str, Any] | None = None,\n task_logs_list: list[LogEntry] | None = None,\n ):\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n log_entry = LogEntry(\n level=level,\n message=message,\n source=source,\n metadata=metadata,\n context=context,\n )\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n if task_logs_list is not None:\n task_logs_list.append(log_entry)\n # Add to buffer for batch persistence\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].append(log_entry)\n # Notify subscribers (for real-time WebSocket updates)\n if task_id in self.subscribers:\n for queue in self.subscribers[task_id]:\n self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)\n # #endregion add_log\n\n # #region subscribe_logs [C:2] [TYPE Function] [SEMANTICS subscribe,queue,realtime]\n # @BRIEF Subscribes to real-time logs for a task.\n # @POST Returns an asyncio.Queue for log entries.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if task_id not in self.subscribers:\n self.subscribers[task_id] = []\n self.subscribers[task_id].append(queue)\n return queue\n # #endregion subscribe_logs\n\n # #region unsubscribe_logs [C:2] [TYPE Function] [SEMANTICS unsubscribe,queue,remove]\n # @BRIEF Unsubscribes from real-time logs for a task.\n # @POST Queue removed from subscribers.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n if task_id in self.subscribers:\n if queue in self.subscribers[task_id]:\n self.subscribers[task_id].remove(queue)\n if not self.subscribers[task_id]:\n del self.subscribers[task_id]\n # #endregion unsubscribe_logs\n\n # #region get_task_logs [C:3] [TYPE Function] [SEMANTICS logs,read,persistence,backfill]\n # @BRIEF Retrieves logs for a specific task (from memory for running, persistence for completed).\n # @RELATION CALLS -> [TaskLogPersistenceService.get_logs]\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None, task_status=None, task_logs: list | None = None\n ) -> list[LogEntry]:\n from .models import TaskStatus\n # For completed tasks, fetch from persistence\n if task_status in [TaskStatus.SUCCESS, TaskStatus.FAILED]:\n if log_filter is None:\n log_filter = LogFilter()\n task_logs_persisted = self.log_persistence_service.get_logs(task_id, log_filter)\n return [\n LogEntry(\n timestamp=log.timestamp,\n level=log.level,\n message=log.message,\n source=log.source,\n metadata=log.metadata,\n )\n for log in task_logs_persisted\n ]\n # For running/pending tasks, return from memory\n return task_logs if task_logs else []\n # #endregion get_task_logs\n\n # #region get_task_log_stats [C:2] [TYPE Function] [SEMANTICS log,stats,aggregate]\n # @BRIEF Get statistics about logs for a task.\n # @RELATION CALLS -> [TaskLogPersistenceService.get_log_stats]\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.log_persistence_service.get_log_stats(task_id)\n # #endregion get_task_log_stats\n\n # #region get_task_log_sources [C:2] [TYPE Function] [SEMANTICS log,sources,unique]\n # @BRIEF Get unique sources for a task's logs.\n # @RELATION CALLS -> [TaskLogPersistenceService.get_sources]\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.log_persistence_service.get_sources(task_id)\n # #endregion get_task_log_sources\n\n # #region delete_logs_for_tasks [C:2] [TYPE Function] [SEMANTICS log,delete,cleanup]\n # @BRIEF Delete logs for specified tasks.\n # @RELATION CALLS -> [TaskLogPersistenceService.delete_logs_for_tasks]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if task_ids:\n self.log_persistence_service.delete_logs_for_tasks(task_ids)\n # #endregion delete_logs_for_tasks\n# #endregion EventBus\n# #endregion EventBusModule\n" + }, + { + "contract_id": "EventBus", + "contract_type": "Class", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 32, + "end_line": 228, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region EventBus [C:5] [TYPE Class] [SEMANTICS event,bus,log,buffer,subscriber,flush]\n# @BRIEF Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out\n# for real-time observers.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [should_log_task_level]\n# @PRE asyncio loop is available for thread-safe queue operations.\n# @POST Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers.\n# @INVARIANT Buffered logs are retried on persistence failure.\n# @RATIONALE Extracted as standalone pub/sub event bus to isolate threading (background flusher\n# thread, buffer locks) and subscription management from task lifecycle and registry.\n# @REJECTED Keeping log buffering inside TaskManager was rejected — the background flusher\n# thread and subscriber queues are architecturally distinct from task state machines\n# and should be independently stoppable/testable.\nclass EventBus:\n \"\"\"Task-scoped log buffering, persistence flushes, and subscriber fan-out.\"\"\"\n\n LOG_FLUSH_INTERVAL = 2.0\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize empty log buffer, subscriber registry, and start background flusher.\n def __init__(self, log_persistence_service: TaskLogPersistenceService, loop: asyncio.AbstractEventLoop):\n self.log_persistence_service = log_persistence_service\n self.loop = loop\n self.subscribers: dict[str, list[asyncio.Queue]] = {}\n self._log_buffer: dict[str, list[LogEntry]] = {}\n self._log_buffer_lock = threading.Lock()\n self._flusher_stop_event = threading.Event()\n self._flusher_thread = threading.Thread(\n target=self._flusher_loop, daemon=True\n )\n self._flusher_thread.start()\n # #endregion __init__\n\n # #region stop [TYPE Function]\n # @BRIEF Signal the flusher thread to stop and wait for it.\n def stop(self) -> None:\n self._flusher_stop_event.set()\n self._flusher_thread.join(timeout=2)\n # #endregion stop\n\n # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Background thread that periodically flushes log buffer to database.\n def _flusher_loop(self):\n seed_trace_id()\n while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n # #endregion _flusher_loop\n\n # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Flush all buffered logs to the database.\n # @RELATION CALLS -> [TaskLogPersistenceService.add_logs]\n def _flush_logs(self):\n seed_trace_id()\n with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n logger.debug(f\"Flushed {len(logs)} logs for task {task_id}\")\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].extend(logs)\n # #endregion _flush_logs\n\n # #region flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Flush logs for a specific task immediately.\n # @PRE task_id exists.\n # @POST Task's buffered logs are written to database.\n # @RELATION CALLS -> [TaskLogPersistenceService.add_logs]\n def flush_task_logs(self, task_id: str):\n with belief_scope(\"EventBus.flush_task_logs\"):\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # #endregion flush_task_logs\n\n # #region add_log [C:3] [TYPE Function] [SEMANTICS log,buffer,notify,subscriber]\n # @BRIEF Adds a log entry to a task buffer and notifies subscribers.\n # @PRE Task exists.\n # @POST Log added to buffer and pushed to queues (if level meets task_log_level filter).\n # @RELATION CALLS -> [should_log_task_level]\n def add_log(\n self,\n task_id: str,\n level: str,\n message: str,\n source: str = \"system\",\n metadata: dict[str, Any] | None = None,\n context: dict[str, Any] | None = None,\n task_logs_list: list[LogEntry] | None = None,\n ):\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n log_entry = LogEntry(\n level=level,\n message=message,\n source=source,\n metadata=metadata,\n context=context,\n )\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n if task_logs_list is not None:\n task_logs_list.append(log_entry)\n # Add to buffer for batch persistence\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].append(log_entry)\n # Notify subscribers (for real-time WebSocket updates)\n if task_id in self.subscribers:\n for queue in self.subscribers[task_id]:\n self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)\n # #endregion add_log\n\n # #region subscribe_logs [C:2] [TYPE Function] [SEMANTICS subscribe,queue,realtime]\n # @BRIEF Subscribes to real-time logs for a task.\n # @POST Returns an asyncio.Queue for log entries.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if task_id not in self.subscribers:\n self.subscribers[task_id] = []\n self.subscribers[task_id].append(queue)\n return queue\n # #endregion subscribe_logs\n\n # #region unsubscribe_logs [C:2] [TYPE Function] [SEMANTICS unsubscribe,queue,remove]\n # @BRIEF Unsubscribes from real-time logs for a task.\n # @POST Queue removed from subscribers.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n if task_id in self.subscribers:\n if queue in self.subscribers[task_id]:\n self.subscribers[task_id].remove(queue)\n if not self.subscribers[task_id]:\n del self.subscribers[task_id]\n # #endregion unsubscribe_logs\n\n # #region get_task_logs [C:3] [TYPE Function] [SEMANTICS logs,read,persistence,backfill]\n # @BRIEF Retrieves logs for a specific task (from memory for running, persistence for completed).\n # @RELATION CALLS -> [TaskLogPersistenceService.get_logs]\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None, task_status=None, task_logs: list | None = None\n ) -> list[LogEntry]:\n from .models import TaskStatus\n # For completed tasks, fetch from persistence\n if task_status in [TaskStatus.SUCCESS, TaskStatus.FAILED]:\n if log_filter is None:\n log_filter = LogFilter()\n task_logs_persisted = self.log_persistence_service.get_logs(task_id, log_filter)\n return [\n LogEntry(\n timestamp=log.timestamp,\n level=log.level,\n message=log.message,\n source=log.source,\n metadata=log.metadata,\n )\n for log in task_logs_persisted\n ]\n # For running/pending tasks, return from memory\n return task_logs if task_logs else []\n # #endregion get_task_logs\n\n # #region get_task_log_stats [C:2] [TYPE Function] [SEMANTICS log,stats,aggregate]\n # @BRIEF Get statistics about logs for a task.\n # @RELATION CALLS -> [TaskLogPersistenceService.get_log_stats]\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.log_persistence_service.get_log_stats(task_id)\n # #endregion get_task_log_stats\n\n # #region get_task_log_sources [C:2] [TYPE Function] [SEMANTICS log,sources,unique]\n # @BRIEF Get unique sources for a task's logs.\n # @RELATION CALLS -> [TaskLogPersistenceService.get_sources]\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.log_persistence_service.get_sources(task_id)\n # #endregion get_task_log_sources\n\n # #region delete_logs_for_tasks [C:2] [TYPE Function] [SEMANTICS log,delete,cleanup]\n # @BRIEF Delete logs for specified tasks.\n # @RELATION CALLS -> [TaskLogPersistenceService.delete_logs_for_tasks]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if task_ids:\n self.log_persistence_service.delete_logs_for_tasks(task_ids)\n # #endregion delete_logs_for_tasks\n# #endregion EventBus\n" + }, + { + "contract_id": "flush_task_logs", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 104, + "end_line": 118, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Task's buffered logs are written to database.", + "PRE": "task_id exists.", + "PURPOSE": "Flush logs for a specific task immediately." + }, + "relations": [ + { + "source_id": "flush_task_logs", + "relation_type": "CALLS", + "target_id": "TaskLogPersistenceService.add_logs", + "target_ref": "[TaskLogPersistenceService.add_logs]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Flush logs for a specific task immediately.\n # @PRE task_id exists.\n # @POST Task's buffered logs are written to database.\n # @RELATION CALLS -> [TaskLogPersistenceService.add_logs]\n def flush_task_logs(self, task_id: str):\n with belief_scope(\"EventBus.flush_task_logs\"):\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # #endregion flush_task_logs\n" + }, + { + "contract_id": "add_log", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 120, + "end_line": 157, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Log added to buffer and pushed to queues (if level meets task_log_level filter).", + "PRE": "Task exists.", + "PURPOSE": "Adds a log entry to a task buffer and notifies subscribers." + }, + "relations": [ + { + "source_id": "add_log", + "relation_type": "CALLS", + "target_id": "should_log_task_level", + "target_ref": "[should_log_task_level]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region add_log [C:3] [TYPE Function] [SEMANTICS log,buffer,notify,subscriber]\n # @BRIEF Adds a log entry to a task buffer and notifies subscribers.\n # @PRE Task exists.\n # @POST Log added to buffer and pushed to queues (if level meets task_log_level filter).\n # @RELATION CALLS -> [should_log_task_level]\n def add_log(\n self,\n task_id: str,\n level: str,\n message: str,\n source: str = \"system\",\n metadata: dict[str, Any] | None = None,\n context: dict[str, Any] | None = None,\n task_logs_list: list[LogEntry] | None = None,\n ):\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n log_entry = LogEntry(\n level=level,\n message=message,\n source=source,\n metadata=metadata,\n context=context,\n )\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n if task_logs_list is not None:\n task_logs_list.append(log_entry)\n # Add to buffer for batch persistence\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].append(log_entry)\n # Notify subscribers (for real-time WebSocket updates)\n if task_id in self.subscribers:\n for queue in self.subscribers[task_id]:\n self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)\n # #endregion add_log\n" + }, + { + "contract_id": "subscribe_logs", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 159, + "end_line": 168, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Returns an asyncio.Queue for log entries.", + "PURPOSE": "Subscribes to real-time logs for a task." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region subscribe_logs [C:2] [TYPE Function] [SEMANTICS subscribe,queue,realtime]\n # @BRIEF Subscribes to real-time logs for a task.\n # @POST Returns an asyncio.Queue for log entries.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if task_id not in self.subscribers:\n self.subscribers[task_id] = []\n self.subscribers[task_id].append(queue)\n return queue\n # #endregion subscribe_logs\n" + }, + { + "contract_id": "unsubscribe_logs", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 170, + "end_line": 179, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Queue removed from subscribers.", + "PURPOSE": "Unsubscribes from real-time logs for a task." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region unsubscribe_logs [C:2] [TYPE Function] [SEMANTICS unsubscribe,queue,remove]\n # @BRIEF Unsubscribes from real-time logs for a task.\n # @POST Queue removed from subscribers.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n if task_id in self.subscribers:\n if queue in self.subscribers[task_id]:\n self.subscribers[task_id].remove(queue)\n if not self.subscribers[task_id]:\n del self.subscribers[task_id]\n # #endregion unsubscribe_logs\n" + }, + { + "contract_id": "get_task_logs", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 181, + "end_line": 205, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Retrieves logs for a specific task (from memory for running, persistence for completed)." + }, + "relations": [ + { + "source_id": "get_task_logs", + "relation_type": "CALLS", + "target_id": "TaskLogPersistenceService.get_logs", + "target_ref": "[TaskLogPersistenceService.get_logs]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region get_task_logs [C:3] [TYPE Function] [SEMANTICS logs,read,persistence,backfill]\n # @BRIEF Retrieves logs for a specific task (from memory for running, persistence for completed).\n # @RELATION CALLS -> [TaskLogPersistenceService.get_logs]\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None, task_status=None, task_logs: list | None = None\n ) -> list[LogEntry]:\n from .models import TaskStatus\n # For completed tasks, fetch from persistence\n if task_status in [TaskStatus.SUCCESS, TaskStatus.FAILED]:\n if log_filter is None:\n log_filter = LogFilter()\n task_logs_persisted = self.log_persistence_service.get_logs(task_id, log_filter)\n return [\n LogEntry(\n timestamp=log.timestamp,\n level=log.level,\n message=log.message,\n source=log.source,\n metadata=log.metadata,\n )\n for log in task_logs_persisted\n ]\n # For running/pending tasks, return from memory\n return task_logs if task_logs else []\n # #endregion get_task_logs\n" + }, + { + "contract_id": "get_task_log_stats", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 207, + "end_line": 212, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Get statistics about logs for a task." + }, + "relations": [ + { + "source_id": "get_task_log_stats", + "relation_type": "CALLS", + "target_id": "TaskLogPersistenceService.get_log_stats", + "target_ref": "[TaskLogPersistenceService.get_log_stats]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region get_task_log_stats [C:2] [TYPE Function] [SEMANTICS log,stats,aggregate]\n # @BRIEF Get statistics about logs for a task.\n # @RELATION CALLS -> [TaskLogPersistenceService.get_log_stats]\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.log_persistence_service.get_log_stats(task_id)\n # #endregion get_task_log_stats\n" + }, + { + "contract_id": "get_task_log_sources", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 214, + "end_line": 219, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Get unique sources for a task's logs." + }, + "relations": [ + { + "source_id": "get_task_log_sources", + "relation_type": "CALLS", + "target_id": "TaskLogPersistenceService.get_sources", + "target_ref": "[TaskLogPersistenceService.get_sources]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region get_task_log_sources [C:2] [TYPE Function] [SEMANTICS log,sources,unique]\n # @BRIEF Get unique sources for a task's logs.\n # @RELATION CALLS -> [TaskLogPersistenceService.get_sources]\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.log_persistence_service.get_sources(task_id)\n # #endregion get_task_log_sources\n" + }, + { + "contract_id": "delete_logs_for_tasks", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/event_bus.py", + "start_line": 221, + "end_line": 227, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Delete logs for specified tasks." + }, + "relations": [ + { + "source_id": "delete_logs_for_tasks", + "relation_type": "CALLS", + "target_id": "TaskLogPersistenceService.delete_logs_for_tasks", + "target_ref": "[TaskLogPersistenceService.delete_logs_for_tasks]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region delete_logs_for_tasks [C:2] [TYPE Function] [SEMANTICS log,delete,cleanup]\n # @BRIEF Delete logs for specified tasks.\n # @RELATION CALLS -> [TaskLogPersistenceService.delete_logs_for_tasks]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if task_ids:\n self.log_persistence_service.delete_logs_for_tasks(task_ids)\n # #endregion delete_logs_for_tasks\n" + }, + { + "contract_id": "TaskGraphModule", + "contract_type": "Module", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 1, + "end_line": 150, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "In-memory task registry with persistence-backed hydration, pagination, and" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TaskGraphModule [C:5] [TYPE Module] [SEMANTICS task,graph,registry,persistence,crud]\n# @BRIEF In-memory task registry with persistence-backed hydration, pagination, and\n# status/plugin filtering — extracted from the monolithic TaskManager.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [Task]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskStatus]\n# @RATIONALE Extracted from TaskManager to satisfy INV_7. TaskGraph owns the in-memory task\n# dictionary and provides task CRUD operations independent of execution, log flush,\n# or event subscription concerns.\n# @REJECTED Keeping task registry inside TaskManager was rejected — it forces all five concerns\n# (registry, lifecycle, event bus, log flush, subscriptions) into one 708-line module\n# violating INV_7. TaskGraph is a pure data registry with persistence hydration.\n# @PRE TaskPersistenceService is initialized and provides load_tasks / delete_tasks.\n# @POST Task registry membership is keyed by unique task id and survives persistence hydration.\n# @SIDE_EFFECT Mutates in-memory task dictionary; calls persistence on load/delete.\n# @DATA_CONTRACT Input[task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]]\n# @INVARIANT Registry membership is keyed by unique task id.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom .models import Task, TaskStatus\nfrom .persistence import TaskPersistenceService\n\n\n# #region TaskGraph [C:5] [TYPE Class] [SEMANTICS task,registry,crud,persistence,filter]\n# @BRIEF In-memory task dependency graph spanning task registry nodes, pause futures,\n# and persistence-backed hydration.\n# @RELATION DEPENDS_ON -> [Task]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE TaskPersistenceService is available for load/delete operations.\n# @POST Each live task id resolves to at most one active Task node and optional pause future.\n# @INVARIANT Registry membership is keyed by unique task id.\n# @RATIONALE Extracted as standalone registry class to decouple task CRUD from execution and\n# logging concerns. TaskGraph is a pure data structure with no async or I/O beyond\n# persistence hydration.\n# @REJECTED Keeping task dict in TaskManager was rejected — it forced every operation to\n# navigate through the god class, preventing independent testing and violating INV_7.\nclass TaskGraph:\n \"\"\"In-memory task registry with persistence-backed hydration and CRUD.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize empty task registry with persistence service.\n def __init__(self, persistence_service: TaskPersistenceService):\n self.tasks: dict[str, Task] = {}\n self.task_futures: dict[str, Any] = {}\n self.persistence_service = persistence_service\n # #endregion __init__\n\n # #region load_persisted_tasks [C:3] [TYPE Function] [SEMANTICS persistence,load,hydration]\n # @BRIEF Load persisted tasks using persistence service.\n # @RELATION CALLS -> [TaskPersistenceService.load_tasks]\n def load_persisted_tasks(self, limit: int = 100) -> None:\n loaded_tasks = self.persistence_service.load_tasks(limit=limit)\n for task in loaded_tasks:\n if task.id not in self.tasks:\n self.tasks[task.id] = task\n # #endregion load_persisted_tasks\n\n # #region get_task [C:2] [TYPE Function] [SEMANTICS task,get,registry]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.tasks.get(task_id)\n # #endregion get_task\n\n # #region get_all_tasks [C:1] [TYPE Function]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return list(self.tasks.values())\n # #endregion get_all_tasks\n\n # #region get_tasks [C:3] [TYPE Function] [SEMANTICS task,filter,paginate,sort]\n # @BRIEF Retrieves tasks with pagination and optional status/plugin filters.\n # @RELATION DEPENDS_ON -> [Task]\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n tasks = list(self.tasks.values())\n if status:\n tasks = [t for t in tasks if t.status == status]\n if plugin_ids:\n plugin_id_set = set(plugin_ids)\n tasks = [t for t in tasks if t.plugin_id in plugin_id_set]\n if completed_only:\n tasks = [\n t for t in tasks if t.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]\n ]\n # Sort by started_at descending with tolerant handling of mixed tz-aware/naive values.\n def sort_key(task: Task) -> float:\n started_at = task.started_at\n if started_at is None:\n return float(\"-inf\")\n if not isinstance(started_at, datetime):\n return float(\"-inf\")\n if started_at.tzinfo is None:\n return started_at.replace(tzinfo=UTC).timestamp()\n return started_at.timestamp()\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n # #endregion get_tasks\n\n # #region add_task [C:2] [TYPE Function] [SEMANTICS task,register]\n # @BRIEF Register a task in the in-memory registry.\n def add_task(self, task: Task) -> None:\n self.tasks[task.id] = task\n # #endregion add_task\n\n # #region remove_tasks [C:3] [TYPE Function] [SEMANTICS task,remove,clear,persistence]\n # @BRIEF Remove tasks from registry and persistence, cancel futures for waiting tasks.\n # @RELATION CALLS -> [TaskPersistenceService.delete_tasks]\n def remove_tasks(self, task_ids: list[str]) -> int:\n for tid in task_ids:\n if tid in self.task_futures:\n self.task_futures[tid].cancel()\n del self.task_futures[tid]\n if tid in self.tasks:\n del self.tasks[tid]\n if task_ids:\n self.persistence_service.delete_tasks(task_ids)\n return len(task_ids)\n # #endregion remove_tasks\n\n # #region create_future [C:1] [TYPE Function]\n # @BRIEF Create and store a future for a paused task.\n def create_future(self, task_id: str, future: Any) -> None:\n self.task_futures[task_id] = future\n # #endregion create_future\n\n # #region resolve_future [C:1] [TYPE Function]\n # @BRIEF Resolve a paused task's future and remove it from the map.\n def resolve_future(self, task_id: str, result: bool = True) -> None:\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(result)\n del self.task_futures[task_id]\n # #endregion resolve_future\n\n # #region remove_future [C:1] [TYPE Function]\n # @BRIEF Remove a future without resolving it.\n def remove_future(self, task_id: str) -> None:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion remove_future\n# #endregion TaskGraph\n# #endregion TaskGraphModule\n" + }, + { + "contract_id": "TaskGraph", + "contract_type": "Class", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 27, + "end_line": 149, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "In-memory task dependency graph spanning task registry nodes, pause futures," + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TaskGraph [C:5] [TYPE Class] [SEMANTICS task,registry,crud,persistence,filter]\n# @BRIEF In-memory task dependency graph spanning task registry nodes, pause futures,\n# and persistence-backed hydration.\n# @RELATION DEPENDS_ON -> [Task]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE TaskPersistenceService is available for load/delete operations.\n# @POST Each live task id resolves to at most one active Task node and optional pause future.\n# @INVARIANT Registry membership is keyed by unique task id.\n# @RATIONALE Extracted as standalone registry class to decouple task CRUD from execution and\n# logging concerns. TaskGraph is a pure data structure with no async or I/O beyond\n# persistence hydration.\n# @REJECTED Keeping task dict in TaskManager was rejected — it forced every operation to\n# navigate through the god class, preventing independent testing and violating INV_7.\nclass TaskGraph:\n \"\"\"In-memory task registry with persistence-backed hydration and CRUD.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize empty task registry with persistence service.\n def __init__(self, persistence_service: TaskPersistenceService):\n self.tasks: dict[str, Task] = {}\n self.task_futures: dict[str, Any] = {}\n self.persistence_service = persistence_service\n # #endregion __init__\n\n # #region load_persisted_tasks [C:3] [TYPE Function] [SEMANTICS persistence,load,hydration]\n # @BRIEF Load persisted tasks using persistence service.\n # @RELATION CALLS -> [TaskPersistenceService.load_tasks]\n def load_persisted_tasks(self, limit: int = 100) -> None:\n loaded_tasks = self.persistence_service.load_tasks(limit=limit)\n for task in loaded_tasks:\n if task.id not in self.tasks:\n self.tasks[task.id] = task\n # #endregion load_persisted_tasks\n\n # #region get_task [C:2] [TYPE Function] [SEMANTICS task,get,registry]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.tasks.get(task_id)\n # #endregion get_task\n\n # #region get_all_tasks [C:1] [TYPE Function]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return list(self.tasks.values())\n # #endregion get_all_tasks\n\n # #region get_tasks [C:3] [TYPE Function] [SEMANTICS task,filter,paginate,sort]\n # @BRIEF Retrieves tasks with pagination and optional status/plugin filters.\n # @RELATION DEPENDS_ON -> [Task]\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n tasks = list(self.tasks.values())\n if status:\n tasks = [t for t in tasks if t.status == status]\n if plugin_ids:\n plugin_id_set = set(plugin_ids)\n tasks = [t for t in tasks if t.plugin_id in plugin_id_set]\n if completed_only:\n tasks = [\n t for t in tasks if t.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]\n ]\n # Sort by started_at descending with tolerant handling of mixed tz-aware/naive values.\n def sort_key(task: Task) -> float:\n started_at = task.started_at\n if started_at is None:\n return float(\"-inf\")\n if not isinstance(started_at, datetime):\n return float(\"-inf\")\n if started_at.tzinfo is None:\n return started_at.replace(tzinfo=UTC).timestamp()\n return started_at.timestamp()\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n # #endregion get_tasks\n\n # #region add_task [C:2] [TYPE Function] [SEMANTICS task,register]\n # @BRIEF Register a task in the in-memory registry.\n def add_task(self, task: Task) -> None:\n self.tasks[task.id] = task\n # #endregion add_task\n\n # #region remove_tasks [C:3] [TYPE Function] [SEMANTICS task,remove,clear,persistence]\n # @BRIEF Remove tasks from registry and persistence, cancel futures for waiting tasks.\n # @RELATION CALLS -> [TaskPersistenceService.delete_tasks]\n def remove_tasks(self, task_ids: list[str]) -> int:\n for tid in task_ids:\n if tid in self.task_futures:\n self.task_futures[tid].cancel()\n del self.task_futures[tid]\n if tid in self.tasks:\n del self.tasks[tid]\n if task_ids:\n self.persistence_service.delete_tasks(task_ids)\n return len(task_ids)\n # #endregion remove_tasks\n\n # #region create_future [C:1] [TYPE Function]\n # @BRIEF Create and store a future for a paused task.\n def create_future(self, task_id: str, future: Any) -> None:\n self.task_futures[task_id] = future\n # #endregion create_future\n\n # #region resolve_future [C:1] [TYPE Function]\n # @BRIEF Resolve a paused task's future and remove it from the map.\n def resolve_future(self, task_id: str, result: bool = True) -> None:\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(result)\n del self.task_futures[task_id]\n # #endregion resolve_future\n\n # #region remove_future [C:1] [TYPE Function]\n # @BRIEF Remove a future without resolving it.\n def remove_future(self, task_id: str) -> None:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion remove_future\n# #endregion TaskGraph\n" + }, + { + "contract_id": "load_persisted_tasks", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 51, + "end_line": 59, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Load persisted tasks using persistence service." + }, + "relations": [ + { + "source_id": "load_persisted_tasks", + "relation_type": "CALLS", + "target_id": "TaskPersistenceService.load_tasks", + "target_ref": "[TaskPersistenceService.load_tasks]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region load_persisted_tasks [C:3] [TYPE Function] [SEMANTICS persistence,load,hydration]\n # @BRIEF Load persisted tasks using persistence service.\n # @RELATION CALLS -> [TaskPersistenceService.load_tasks]\n def load_persisted_tasks(self, limit: int = 100) -> None:\n loaded_tasks = self.persistence_service.load_tasks(limit=limit)\n for task in loaded_tasks:\n if task.id not in self.tasks:\n self.tasks[task.id] = task\n # #endregion load_persisted_tasks\n" + }, + { + "contract_id": "get_task", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 61, + "end_line": 65, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Retrieves a task by its ID." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region get_task [C:2] [TYPE Function] [SEMANTICS task,get,registry]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.tasks.get(task_id)\n # #endregion get_task\n" + }, + { + "contract_id": "get_all_tasks", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 67, + "end_line": 71, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Retrieves all registered tasks." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region get_all_tasks [C:1] [TYPE Function]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return list(self.tasks.values())\n # #endregion get_all_tasks\n" + }, + { + "contract_id": "get_tasks", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 73, + "end_line": 106, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Retrieves tasks with pagination and optional status/plugin filters." + }, + "relations": [ + { + "source_id": "get_tasks", + "relation_type": "DEPENDS_ON", + "target_id": "Task", + "target_ref": "[Task]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region get_tasks [C:3] [TYPE Function] [SEMANTICS task,filter,paginate,sort]\n # @BRIEF Retrieves tasks with pagination and optional status/plugin filters.\n # @RELATION DEPENDS_ON -> [Task]\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n tasks = list(self.tasks.values())\n if status:\n tasks = [t for t in tasks if t.status == status]\n if plugin_ids:\n plugin_id_set = set(plugin_ids)\n tasks = [t for t in tasks if t.plugin_id in plugin_id_set]\n if completed_only:\n tasks = [\n t for t in tasks if t.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]\n ]\n # Sort by started_at descending with tolerant handling of mixed tz-aware/naive values.\n def sort_key(task: Task) -> float:\n started_at = task.started_at\n if started_at is None:\n return float(\"-inf\")\n if not isinstance(started_at, datetime):\n return float(\"-inf\")\n if started_at.tzinfo is None:\n return started_at.replace(tzinfo=UTC).timestamp()\n return started_at.timestamp()\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n # #endregion get_tasks\n" + }, + { + "contract_id": "add_task", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 108, + "end_line": 112, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Register a task in the in-memory registry." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region add_task [C:2] [TYPE Function] [SEMANTICS task,register]\n # @BRIEF Register a task in the in-memory registry.\n def add_task(self, task: Task) -> None:\n self.tasks[task.id] = task\n # #endregion add_task\n" + }, + { + "contract_id": "remove_tasks", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 114, + "end_line": 127, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Remove tasks from registry and persistence, cancel futures for waiting tasks." + }, + "relations": [ + { + "source_id": "remove_tasks", + "relation_type": "CALLS", + "target_id": "TaskPersistenceService.delete_tasks", + "target_ref": "[TaskPersistenceService.delete_tasks]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region remove_tasks [C:3] [TYPE Function] [SEMANTICS task,remove,clear,persistence]\n # @BRIEF Remove tasks from registry and persistence, cancel futures for waiting tasks.\n # @RELATION CALLS -> [TaskPersistenceService.delete_tasks]\n def remove_tasks(self, task_ids: list[str]) -> int:\n for tid in task_ids:\n if tid in self.task_futures:\n self.task_futures[tid].cancel()\n del self.task_futures[tid]\n if tid in self.tasks:\n del self.tasks[tid]\n if task_ids:\n self.persistence_service.delete_tasks(task_ids)\n return len(task_ids)\n # #endregion remove_tasks\n" + }, + { + "contract_id": "create_future", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 129, + "end_line": 133, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Create and store a future for a paused task." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region create_future [C:1] [TYPE Function]\n # @BRIEF Create and store a future for a paused task.\n def create_future(self, task_id: str, future: Any) -> None:\n self.task_futures[task_id] = future\n # #endregion create_future\n" + }, + { + "contract_id": "resolve_future", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 135, + "end_line": 141, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Resolve a paused task's future and remove it from the map." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region resolve_future [C:1] [TYPE Function]\n # @BRIEF Resolve a paused task's future and remove it from the map.\n def resolve_future(self, task_id: str, result: bool = True) -> None:\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(result)\n del self.task_futures[task_id]\n # #endregion resolve_future\n" + }, + { + "contract_id": "remove_future", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/graph.py", + "start_line": 143, + "end_line": 148, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Remove a future without resolving it." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region remove_future [C:1] [TYPE Function]\n # @BRIEF Remove a future without resolving it.\n def remove_future(self, task_id: str) -> None:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion remove_future\n" + }, + { + "contract_id": "JobLifecycleModule", + "contract_type": "Module", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 1, + "end_line": 313, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "Task creation, execution, pause/resume, and completion transitions for plugin-backed" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region JobLifecycleModule [C:5] [TYPE Module] [SEMANTICS task,lifecycle,state,machine,execution]\n# @BRIEF Task creation, execution, pause/resume, and completion transitions for plugin-backed\n# jobs — extracted from the monolithic TaskManager.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RATIONALE Extracted from TaskManager to satisfy INV_7. JobLifecycle encodes the task state\n# machine with plugin execution, context injection, backward-compatible plugin\n# dispatch, and input/mapping pause/resume transitions.\n# @REJECTED Keeping lifecycle/execution logic inside TaskManager was rejected — it mixes\n# async execution orchestration with passive registry and log buffering concerns,\n# violating single-responsibility and INV_7.\n# @PRE Requested plugin ids resolve to executable plugins; task state transitions target known\n# TaskStatus values.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @SIDE_EFFECT Schedules async execution, pauses on input/mapping requests, mutates persisted\n# task lifecycle markers.\n# @INVARIANT A task cannot be resumed from a waiting state unless a matching future exists or\n# a new wait future is created.\n\nimport asyncio\nimport inspect\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ..cot_logger import seed_trace_id\nfrom ..logger import belief_scope, logger\nfrom .context import TaskContext\nfrom .graph import TaskGraph\nfrom .models import Task, TaskStatus\n\n\n# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]\n# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for\n# plugin-backed jobs.\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution\n# orchestration (plugin dispatch, context injection, pause/resume futures) from\n# passive registry and log buffering concerns.\n# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task\n# orchestration with synchronous registry operations and background threading,\n# violating single-responsibility and making the 708-line module unmaintainable.\nclass JobLifecycle:\n \"\"\"Task state machine: creation, execution, pause/resume, completion.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with dependencies.\n def __init__(\n self,\n plugin_loader: Any,\n graph: TaskGraph,\n event_bus: Any,\n persistence_service: Any,\n executor: ThreadPoolExecutor,\n loop: asyncio.AbstractEventLoop,\n ):\n self.plugin_loader = plugin_loader\n self.graph = graph\n self.event_bus = event_bus\n self.persistence_service = persistence_service\n self.executor = executor\n self.loop = loop\n # #endregion __init__\n\n # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n\n # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # #endregion _run_task\n\n # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n\n # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n\n # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n\n # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n# #endregion JobLifecycle\n# #endregion JobLifecycleModule\n" + }, + { + "contract_id": "JobLifecycle", + "contract_type": "Class", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 38, + "end_line": 312, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "Encodes task creation, execution, pause/resume, and completion transitions for" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]\n# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for\n# plugin-backed jobs.\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution\n# orchestration (plugin dispatch, context injection, pause/resume futures) from\n# passive registry and log buffering concerns.\n# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task\n# orchestration with synchronous registry operations and background threading,\n# violating single-responsibility and making the 708-line module unmaintainable.\nclass JobLifecycle:\n \"\"\"Task state machine: creation, execution, pause/resume, completion.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with dependencies.\n def __init__(\n self,\n plugin_loader: Any,\n graph: TaskGraph,\n event_bus: Any,\n persistence_service: Any,\n executor: ThreadPoolExecutor,\n loop: asyncio.AbstractEventLoop,\n ):\n self.plugin_loader = plugin_loader\n self.graph = graph\n self.event_bus = event_bus\n self.persistence_service = persistence_service\n self.executor = executor\n self.loop = loop\n # #endregion __init__\n\n # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n\n # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # #endregion _run_task\n\n # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n\n # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n\n # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n\n # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n# #endregion JobLifecycle\n" + }, + { + "contract_id": "create_task", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 76, + "end_line": 106, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "ERROR": "ValueError if plugin not found or params invalid.", + "POST": "Task is created, added to registry, and scheduled for execution.", + "PRE": "Plugin with plugin_id exists. Params are valid dict.", + "PURPOSE": "Creates and queues a new task for execution." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n" + }, + { + "contract_id": "_run_task", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 108, + "end_line": 193, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Task is executed, status updated to SUCCESS or FAILED.", + "PRE": "Task exists in registry.", + "PURPOSE": "Internal method to execute a task with TaskContext support.", + "SIDE_EFFECT": "Executes plugin code, updates task status and persistence, emits logs." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # #endregion _run_task\n" + }, + { + "contract_id": "resolve_task", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 195, + "end_line": 211, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "ERROR": "ValueError if task not found or not awaiting mapping.", + "POST": "Task status updated to RUNNING, params updated, execution resumed.", + "PRE": "Task exists and is in AWAITING_MAPPING state.", + "PURPOSE": "Resumes a task that is awaiting mapping." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n" + }, + { + "contract_id": "wait_for_resolution", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 213, + "end_line": 230, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Execution pauses until future is set.", + "PRE": "Task exists.", + "PURPOSE": "Pauses execution and waits for a resolution signal." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n" + }, + { + "contract_id": "wait_for_input", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 232, + "end_line": 247, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Execution pauses until future is set via resume_task_with_password.", + "PRE": "Task exists.", + "PURPOSE": "Pauses execution and waits for user input." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n" + }, + { + "contract_id": "await_input", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 249, + "end_line": 276, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "ERROR": "ValueError if task not found or not RUNNING.", + "POST": "Task status changed to AWAITING_INPUT, input_request set, persisted.", + "PRE": "Task exists and is in RUNNING state.", + "PURPOSE": "Transition a task to AWAITING_INPUT state with input request." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n" + }, + { + "contract_id": "resume_task_with_password", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/lifecycle.py", + "start_line": 278, + "end_line": 311, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "ERROR": "ValueError if task not found, not awaiting input, or passwords invalid.", + "POST": "Task status changed to RUNNING, passwords injected, task resumed.", + "PRE": "Task exists and is in AWAITING_INPUT state.", + "PURPOSE": "Resume a task that is awaiting input with provided passwords." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n" + }, + { + "contract_id": "TaskManagerModule", + "contract_type": "Module", + "file_path": "backend/src/core/task_manager/manager.py", + "start_line": 1, + "end_line": 322, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "Thin facade composing TaskGraph (registry), EventBus (log/pub-sub), and JobLifecycle" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TaskManagerModule [C:5] [TYPE Module] [SEMANTICS task, schedule, execution, task-manager]\n# @BRIEF Thin facade composing TaskGraph (registry), EventBus (log/pub-sub), and JobLifecycle\n# (state machine) into a single TaskManager interface for backward compatibility.\n# @LAYER: Core\n# @PRE: Plugin loader and database sessions are initialized.\n# @POST: Orchestrates task execution and persistence.\n# @SIDE_EFFECT: Spawns worker threads and flushes logs to DB.\n# @DATA_CONTRACT: Input[plugin_id, params] -> Model[Task, LogEntry]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT: Task IDs are unique.\n# @TEST_CONTRACT: TaskManagerRuntime -> {\n# required_fields: {plugin_loader: PluginLoader},\n# optional_fields: {},\n# invariants: [\"Must use belief_scope for logging\"]\n# }\n# @TEST_FIXTURE: valid_module -> {\"manager_initialized\": true}\n# @TEST_EDGE: missing_required_field -> {\"plugin_loader\": null}\n# @TEST_EDGE: empty_response -> {\"tasks\": []}\n# @TEST_EDGE: invalid_type -> {\"plugin_loader\": \"string_instead_of_object\"}\n# @TEST_EDGE: external_failure -> {\"db_unavailable\": true}\n# @TEST_INVARIANT: logger_compliance -> verifies: [valid_module]\n# @RATIONALE Decomposed from 708-line monolithic module into four focused modules (TaskGraph,\n# EventBus, JobLifecycle, and this facade) to satisfy INV_7. TaskManager now delegates\n# to sub-services while preserving the public API contract.\n# @REJECTED Keeping all five concerns (registry, lifecycle, log buffer, subscriptions, execution)\n# in one module was rejected — it violated INV_7 (708 lines vs 400 max), mixed threading\n# with async execution, and created an unmaintainable god class.\n\nimport asyncio\nimport threading\nfrom concurrent.futures import ThreadPoolExecutor\nfrom typing import Any\n\nfrom ..logger import belief_scope, logger\nfrom .event_bus import EventBus\nfrom .graph import TaskGraph\nfrom .lifecycle import JobLifecycle\nfrom .models import LogFilter, LogStats, Task, TaskStatus\nfrom .persistence import TaskLogPersistenceService, TaskPersistenceService\n\n\n# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state]\n# @BRIEF Facade composing TaskGraph, EventBus, and JobLifecycle into a single interface.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @PRE: Plugin loader resolves plugin ids and persistence services are available.\n# @POST: In-memory task graph, lifecycle scheduler, and log event bus stay consistent with\n# persisted task state.\n# @INVARIANT: Task IDs are unique within the registry.\n# @INVARIANT: Each task has exactly one status at any time.\n# @INVARIANT: Log entries are never deleted after being added to a task.\n# @SIDE_EFFECT: Spawns worker threads, flushes logs to database, and mutates task states.\n# @DATA_CONTRACT: Input[plugin_id, params] -> Output[Task]\n# @RATIONALE Thin facade — all delegating methods are under 15 lines. Actual business logic\n# (registry CRUD, log buffering, lifecycle state machine) lives in extracted modules.\n# @REJECTED Keeping all five concerns in one class was rejected — it violated INV_7 (708-line\n# manager.py) and made the god class impossible to test or maintain independently.\nclass TaskManager:\n \"\"\"\n Facade composing TaskGraph (registry), EventBus (log/pub-sub), and\n JobLifecycle (state machine) into a single TaskManager interface.\n \"\"\"\n\n # #region __init__ [TYPE Function] [C:5]\n # @BRIEF Initialize sub-services, create add_log callback, start background flusher.\n # @PRE: plugin_loader is initialized.\n # @POST: TaskManager is ready to accept tasks.\n # @SIDE_EFFECT: Starts background flusher thread and loads persisted task state into memory.\n def __init__(self, plugin_loader):\n with belief_scope(\"TaskManager.__init__\"):\n logger.reason(\"Initializing task manager runtime services\")\n self.plugin_loader = plugin_loader\n self.executor = ThreadPoolExecutor(max_workers=5)\n self.persistence_service = TaskPersistenceService()\n self.log_persistence_service = TaskLogPersistenceService()\n\n try:\n self.loop = asyncio.get_running_loop()\n except RuntimeError:\n self.loop = asyncio.get_event_loop()\n\n # Create sub-services\n self.graph = TaskGraph(self.persistence_service)\n self.event_bus = EventBus(self.log_persistence_service, self.loop)\n\n # Create add_log callback that lifecycle and plugins use\n self._add_log = self._make_add_log_callback()\n\n self.lifecycle = JobLifecycle(\n plugin_loader=self.plugin_loader,\n graph=self.graph,\n event_bus=self.event_bus,\n persistence_service=self.persistence_service,\n executor=self.executor,\n loop=self.loop,\n )\n\n # Load persisted tasks on startup\n self.graph.load_persisted_tasks()\n\n # Backward-compatible property aliases for tests\n self.tasks = self.graph.tasks\n self.task_futures = self.graph.task_futures\n self.subscribers = self.event_bus.subscribers\n self._log_buffer = self.event_bus._log_buffer\n self._log_buffer_lock = self.event_bus._log_buffer_lock\n self._flusher_stop_event = self.event_bus._flusher_stop_event\n self._flusher_thread = self.event_bus._flusher_thread\n\n logger.reflect(\n \"Task manager runtime initialized\",\n extra={\"task_count\": len(self.tasks)},\n )\n # #endregion __init__\n\n # #region _make_add_log_callback [C:2] [TYPE Function]\n # @BRIEF Create a closure for adding logs that looks up the task and delegates to EventBus.\n def _make_add_log_callback(self):\n def _add_log(task_id, level, message, source=\"system\", metadata=None, context=None):\n task = self.graph.get_task(task_id)\n if not task:\n return\n self.event_bus.add_log(\n task_id, level, message, source, metadata, context,\n task_logs_list=task.logs,\n )\n return _add_log\n # #endregion _make_add_log_callback\n\n # ── Deprecated legacy aliases (delegate to EventBus) ──\n\n # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Legacy alias delegating to EventBus._flusher_loop.\n def _flusher_loop(self):\n self.event_bus._flusher_loop()\n # #endregion _flusher_loop\n\n # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Legacy alias delegating to EventBus._flush_logs.\n def _flush_logs(self):\n self.event_bus._flush_logs()\n # #endregion _flush_logs\n\n # #region _flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Legacy alias delegating to EventBus.flush_task_logs.\n def _flush_task_logs(self, task_id: str):\n self.event_bus.flush_task_logs(task_id)\n # #endregion _flush_task_logs\n\n # ── Task CRUD delegates to TaskGraph ──\n\n # #region get_task [TYPE Function] [C:2]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.graph.get_task(task_id)\n # #endregion get_task\n\n # #region get_all_tasks [TYPE Function] [C:1]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return self.graph.get_all_tasks()\n # #endregion get_all_tasks\n\n # #region get_tasks [TYPE Function] [C:3]\n # @BRIEF Retrieves tasks with pagination and optional status filter.\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n return self.graph.get_tasks(limit, offset, status, plugin_ids, completed_only)\n # #endregion get_tasks\n\n # #region load_persisted_tasks [TYPE Function] [C:2]\n # @BRIEF Load persisted tasks using persistence service.\n def load_persisted_tasks(self) -> None:\n self.graph.load_persisted_tasks(limit=100)\n # #endregion load_persisted_tasks\n\n # #region clear_tasks [TYPE Function] [C:4]\n # @BRIEF Clears tasks based on status filter (also deletes associated logs).\n # @SIDE_EFFECT Removes tasks from registry and persistence; cancels futures.\n def clear_tasks(self, status: TaskStatus | None = None) -> int:\n with belief_scope(\"TaskManager.clear_tasks\"):\n tasks_to_remove = []\n for task_id, task in list(self.tasks.items()):\n should_remove = False\n if status:\n if task.status == status:\n should_remove = True\n else:\n if task.status not in [\n TaskStatus.RUNNING,\n TaskStatus.AWAITING_INPUT,\n TaskStatus.AWAITING_MAPPING,\n ]:\n should_remove = True\n if should_remove:\n tasks_to_remove.append(task_id)\n\n # Delete logs first, then remove tasks\n if tasks_to_remove:\n self.event_bus.delete_logs_for_tasks(tasks_to_remove)\n removed = self.graph.remove_tasks(tasks_to_remove)\n logger.info(f\"Cleared {removed} tasks.\")\n return removed\n # #endregion clear_tasks\n\n # ── Log delegates to EventBus ──\n\n # #region get_task_logs [TYPE Function] [C:3]\n # @BRIEF Retrieves logs for a specific task (from memory or persistence).\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None\n ) -> list:\n task = self.graph.get_task(task_id)\n task_status = task.status if task else None\n task_logs = task.logs if task else []\n return self.event_bus.get_task_logs(\n task_id, log_filter, task_status=task_status, task_logs=task_logs\n )\n # #endregion get_task_logs\n\n # #region get_task_log_stats [TYPE Function] [C:2]\n # @BRIEF Get statistics about logs for a task.\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.event_bus.get_task_log_stats(task_id)\n # #endregion get_task_log_stats\n\n # #region get_task_log_sources [TYPE Function] [C:2]\n # @BRIEF Get unique sources for a task's logs.\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.event_bus.get_task_log_sources(task_id)\n # #endregion get_task_log_sources\n\n # ── Subscription delegates to EventBus ──\n\n # #region subscribe_logs [TYPE Function] [C:2]\n # @BRIEF Subscribes to real-time logs for a task.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n return await self.event_bus.subscribe_logs(task_id)\n # #endregion subscribe_logs\n\n # #region unsubscribe_logs [TYPE Function] [C:2]\n # @BRIEF Unsubscribes from real-time logs for a task.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n self.event_bus.unsubscribe_logs(task_id, queue)\n # #endregion unsubscribe_logs\n\n # ── Lifecycle delegates to JobLifecycle ──\n\n # #region create_task [TYPE Function] [C:4]\n # @BRIEF Creates and queues a new task for execution.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None\n ) -> Task:\n return await self.lifecycle.create_task(\n plugin_id, params, user_id,\n add_log_callback=self._add_log,\n )\n # #endregion create_task\n\n # #region _run_task [TYPE Function] [C:4]\n # @BRIEF Internal method to execute a task with TaskContext support (delegates to lifecycle).\n async def _run_task(self, task_id: str):\n await self.lifecycle._run_task(task_id, add_log_callback=self._add_log)\n # #endregion _run_task\n\n # #region resolve_task [TYPE Function] [C:3]\n # @BRIEF Resumes a task that is awaiting mapping.\n async def resolve_task(self, task_id: str, resolution_params: dict[str, Any]):\n await self.lifecycle.resolve_task(task_id, resolution_params)\n # #endregion resolve_task\n\n # #region wait_for_resolution [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for a resolution signal.\n async def wait_for_resolution(self, task_id: str):\n await self.lifecycle.wait_for_resolution(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for user input.\n async def wait_for_input(self, task_id: str):\n await self.lifecycle.wait_for_input(task_id)\n # #endregion wait_for_input\n\n # #region await_input [TYPE Function] [C:3]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n def await_input(self, task_id: str, input_request: dict[str, Any]) -> None:\n self.lifecycle.await_input(\n task_id, input_request,\n add_log_callback=self._add_log,\n )\n # #endregion await_input\n\n # #region resume_task_with_password [TYPE Function] [C:3]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str]\n ) -> None:\n self.lifecycle.resume_task_with_password(\n task_id, passwords,\n add_log_callback=self._add_log,\n )\n # #endregion resume_task_with_password\n# #endregion TaskManager\n# #endregion TaskManagerModule\n" }, { "contract_id": "TaskManager", "contract_type": "Class", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 41, - "end_line": 707, + "start_line": 48, + "end_line": 321, "tier": "TIER_3", "complexity": 5, "metadata": { "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[plugin_id, params] -> Output[Task]", - "INVARIANT": "Log entries are never deleted after being added to a task.", "LAYER": "Core", - "POST": "In-memory task graph, lifecycle scheduler, and log event bus stay consistent with persisted task state.", - "PRE": "Plugin loader resolves plugin ids and persistence services are available for task state hydration.", - "PURPOSE": "Manages the lifecycle of tasks, including their creation, execution, and state tracking.", - "SIDE_EFFECT": "Spawns worker threads, flushes logs to database, and mutates task states." + "POST": "In-memory task graph, lifecycle scheduler, and log event bus stay consistent with", + "PRE": "Plugin loader resolves plugin ids and persistence services are available.", + "PURPOSE": "Facade composing TaskGraph, EventBus, and JobLifecycle into a single interface." }, "relations": [ { @@ -37128,26 +37838,159 @@ "actual_complexity": 5, "contract_type": "Class" } + }, + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Class" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state]\n# @BRIEF Manages the lifecycle of tasks, including their creation, execution, and state tracking.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @PRE: Plugin loader resolves plugin ids and persistence services are available for task state hydration.\n# @POST: In-memory task graph, lifecycle scheduler, and log event bus stay consistent with persisted task state.\n# @INVARIANT: Task IDs are unique within the registry.\n# @INVARIANT: Each task has exactly one status at any time.\n# @INVARIANT: Log entries are never deleted after being added to a task.\n# @SIDE_EFFECT: Spawns worker threads, flushes logs to database, and mutates task states.\n# @DATA_CONTRACT: Input[plugin_id, params] -> Output[Task]\nclass TaskManager:\n \"\"\"\n Manages the lifecycle of tasks, including their creation, execution, and state tracking.\n \"\"\"\n # Log flush interval in seconds\n LOG_FLUSH_INTERVAL = 2.0\n # #region TaskGraph [TYPE Block] [C:5]\n # @PURPOSE: Represents the in-memory task dependency graph spanning task registry nodes, paused futures, and persistence-backed hydration.\n # @RELATION: [DEPENDS_ON] ->[Task]\n # @RELATION: [DEPENDS_ON] ->[TaskPersistenceService]\n # @PRE: Task ids are generated before insertion and persisted tasks can be reconstructed into Task models.\n # @POST: Each live task id resolves to at most one active Task node and optional pause future.\n # @SIDE_EFFECT: Mutates the in-memory task registry and loads persisted state during manager startup.\n # @DATA_CONTRACT: Input[Task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]]\n # @INVARIANT: Registry membership is keyed by unique task id and survives log streaming side channels.\n # #endregion TaskGraph\n # #region EventBus [TYPE Block] [C:5]\n # @PURPOSE: Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time observers.\n # @RELATION: [DEPENDS_ON] ->[LogEntry]\n # @RELATION: [DEPENDS_ON] ->[TaskLogPersistenceService]\n # @PRE: Task log records accept structured LogEntry payloads and subscribers consume asyncio queues.\n # @POST: Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers in emission order.\n # @SIDE_EFFECT: Writes task logs to persistence, mutates in-memory buffers, and pushes log entries into subscriber queues.\n # @DATA_CONTRACT: Input[task_id, LogEntry, Queue subscribers] -> Output[persisted log rows, streamed log events]\n # @INVARIANT: Buffered logs are retried on persistence failure and every subscriber receives only task-scoped events.\n # #endregion EventBus\n # #region JobLifecycle [TYPE Block] [C:5]\n # @PURPOSE: Encodes task creation, execution, pause/resume, and completion transitions for plugin-backed jobs.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n # @RELATION: [DEPENDS_ON] ->[TaskContext]\n # @RELATION: [DEPENDS_ON] ->[PluginLoader]\n # @PRE: Requested plugin ids resolve to executable plugins and task state transitions target known TaskStatus values.\n # @POST: Every scheduled task transitions through a valid lifecycle path ending in persisted terminal or waiting state.\n # @SIDE_EFFECT: Schedules async execution, pauses on input/mapping requests, and mutates persisted task lifecycle markers.\n # @DATA_CONTRACT: Input[plugin_id, params, task_id, resolution payloads] -> Output[Task status transitions, task result]\n # @INVARIANT: A task cannot be resumed from a waiting state unless a matching future exists or a new wait future is created.\n # #endregion JobLifecycle\n # #region __init__ [TYPE Function] [C:5]\n # @PURPOSE: Initialize the TaskManager with dependencies.\n # @PRE: plugin_loader is initialized.\n # @POST: TaskManager is ready to accept tasks.\n # @SIDE_EFFECT: Starts background flusher thread and loads persisted task state into memory.\n # @RELATION: [CALLS] ->[TaskPersistenceService.load_tasks]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n # @PARAM: plugin_loader - The plugin loader instance.\n def __init__(self, plugin_loader):\n with belief_scope(\"TaskManager.__init__\"):\n logger.reason(\"Initializing task manager runtime services\")\n self.plugin_loader = plugin_loader\n self.tasks: dict[str, Task] = {}\n self.subscribers: dict[str, list[asyncio.Queue]] = {}\n self.executor = ThreadPoolExecutor(\n max_workers=5\n ) # For CPU-bound plugin execution\n self.persistence_service = TaskPersistenceService()\n self.log_persistence_service = TaskLogPersistenceService()\n # Log buffer: task_id -> List[LogEntry]\n self._log_buffer: dict[str, list[LogEntry]] = {}\n self._log_buffer_lock = threading.Lock()\n # Flusher thread for batch writing logs\n self._flusher_stop_event = threading.Event()\n self._flusher_thread = threading.Thread(\n target=self._flusher_loop, daemon=True\n )\n self._flusher_thread.start()\n try:\n self.loop = asyncio.get_running_loop()\n except RuntimeError:\n self.loop = asyncio.get_event_loop()\n self.task_futures: dict[str, asyncio.Future] = {}\n # Load persisted tasks on startup\n self.load_persisted_tasks()\n logger.reflect(\n \"Task manager runtime initialized\",\n extra={\"task_count\": len(self.tasks)},\n )\n # #endregion __init__\n # #region _flusher_loop [TYPE Function] [C:3]\n # @PURPOSE: Background thread that periodically flushes log buffer to database.\n # @PRE: TaskManager is initialized.\n # @POST: Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.\n # @RELATION: [CALLS] ->[TaskManager._flush_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def _flusher_loop(self):\n \"\"\"Background thread that flushes log buffer to database.\"\"\"\n seed_trace_id() # seed trace_id for the daemon flusher thread\n while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n # #endregion _flusher_loop\n # #region _flush_logs [TYPE Function] [C:3]\n # @PURPOSE: Flush all buffered logs to the database.\n # @PRE: None.\n # @POST: All buffered logs are written to task_logs table.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.add_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def _flush_logs(self):\n \"\"\"Flush all buffered logs to the database.\"\"\"\n seed_trace_id()\n with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n logger.debug(f\"Flushed {len(logs)} logs for task {task_id}\")\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # Re-add logs to buffer on failure\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].extend(logs)\n # #endregion _flush_logs\n # #region _flush_task_logs [TYPE Function] [C:3]\n # @PURPOSE: Flush logs for a specific task immediately.\n # @PRE: task_id exists.\n # @POST: Task's buffered logs are written to database.\n # @PARAM: task_id (str) - The task ID.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.add_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def _flush_task_logs(self, task_id: str):\n \"\"\"Flush logs for a specific task immediately.\"\"\"\n with belief_scope(\"_flush_task_logs\"):\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n except Exception as e:\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n # #endregion _flush_task_logs\n # #region create_task [TYPE Function] [C:3]\n # @PURPOSE: Creates and queues a new task for execution.\n # @PRE: Plugin with plugin_id exists. Params are valid.\n # @POST: Task is created, added to registry, and scheduled for execution.\n # @PARAM: plugin_id (str) - The ID of the plugin to run.\n # @PARAM: params (Dict[str, Any]) - Parameters for the plugin.\n # @PARAM: user_id (Optional[str]) - ID of the user requesting the task.\n # @RETURN: Task - The created task instance.\n # @THROWS: ValueError if plugin not found or params invalid.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None\n ) -> Task:\n with belief_scope(\"TaskManager.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.tasks[task.id] = task\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id)\n ) # Schedule task for execution\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n # #region _run_task [TYPE Function] [C:3]\n # @PURPOSE: Internal method to execute a task with TaskContext support.\n # @PRE: Task exists in registry.\n # @POST: Task is executed, status updated to SUCCESS or FAILED.\n # @PARAM: task_id (str) - The ID of the task to run.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n # @RELATION: [DEPENDS_ON] ->[TaskContext]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n async def _run_task(self, task_id: str):\n seed_trace_id()\n with belief_scope(\"TaskManager._run_task\", f\"task_id={task_id}\"):\n task = self.tasks[task_id]\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n self._add_log(\n task_id,\n \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n # Prepare params and check if plugin supports new TaskContext\n params = {**task.params, \"_task_id\": task_id}\n # Check if plugin's execute method accepts 'context' parameter\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=self._add_log,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n # Backward compatibility: old-style plugins without context\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n self._add_log(\n task_id,\n \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n self._add_log(\n task_id,\n \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n # Flush any remaining buffered logs before persisting task\n self._flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # #endregion _run_task\n # #region resolve_task [TYPE Function] [C:3]\n # @PURPOSE: Resumes a task that is awaiting mapping.\n # @PRE: Task exists and is in AWAITING_MAPPING state.\n # @POST: Task status updated to RUNNING, params updated, execution resumed.\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: resolution_params (Dict[str, Any]) - Params to resolve the wait.\n # @THROWS: ValueError if task not found or not awaiting mapping.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n async def resolve_task(self, task_id: str, resolution_params: dict[str, Any]):\n with belief_scope(\"TaskManager.resolve_task\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n # Update task params with resolution\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self._add_log(task_id, \"INFO\", \"Task resumed after mapping resolution.\")\n # Signal the future to continue\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n # #endregion resolve_task\n # #region wait_for_resolution [TYPE Function] [C:3]\n # @PURPOSE: Pauses execution and waits for a resolution signal.\n # @PRE: Task exists.\n # @POST: Execution pauses until future is set.\n # @PARAM: task_id (str) - The ID of the task.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n async def wait_for_resolution(self, task_id: str):\n with belief_scope(\"TaskManager.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n self.task_futures[task_id] = self.loop.create_future()\n try:\n await self.task_futures[task_id]\n finally:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion wait_for_resolution\n # #region wait_for_input [TYPE Function] [C:3]\n # @PURPOSE: Pauses execution and waits for user input.\n # @PRE: Task exists.\n # @POST: Execution pauses until future is set via resume_task_with_password.\n # @PARAM: task_id (str) - The ID of the task.\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n async def wait_for_input(self, task_id: str):\n with belief_scope(\"TaskManager.wait_for_input\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n return\n # Status is already set to AWAITING_INPUT by await_input()\n self.task_futures[task_id] = self.loop.create_future()\n try:\n await self.task_futures[task_id]\n finally:\n if task_id in self.task_futures:\n del self.task_futures[task_id]\n # #endregion wait_for_input\n # #region get_task [TYPE Function] [C:3]\n # @PURPOSE: Retrieves a task by its ID.\n # @PRE: task_id is a string.\n # @POST: Returns Task object or None.\n # @PARAM: task_id (str) - ID of the task.\n # @RETURN: Optional[Task] - The task or None.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def get_task(self, task_id: str) -> Task | None:\n with belief_scope(\"TaskManager.get_task\", f\"task_id={task_id}\"):\n return self.tasks.get(task_id)\n # #endregion get_task\n # #region get_all_tasks [TYPE Function] [C:3]\n # @PURPOSE: Retrieves all registered tasks.\n # @PRE: None.\n # @POST: Returns list of all Task objects.\n # @RETURN: List[Task] - All tasks.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def get_all_tasks(self) -> list[Task]:\n with belief_scope(\"TaskManager.get_all_tasks\"):\n return list(self.tasks.values())\n # #endregion get_all_tasks\n # #region get_tasks [TYPE Function] [C:3]\n # @PURPOSE: Retrieves tasks with pagination and optional status filter.\n # @PRE: limit and offset are non-negative integers.\n # @POST: Returns a list of tasks sorted by start_time descending.\n # @PARAM: limit (int) - Maximum number of tasks to return.\n # @PARAM: offset (int) - Number of tasks to skip.\n # @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n # @RETURN: List[Task] - List of tasks matching criteria.\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n with belief_scope(\"TaskManager.get_tasks\"):\n tasks = list(self.tasks.values())\n if status:\n tasks = [t for t in tasks if t.status == status]\n if plugin_ids:\n plugin_id_set = set(plugin_ids)\n tasks = [t for t in tasks if t.plugin_id in plugin_id_set]\n if completed_only:\n tasks = [\n t for t in tasks if t.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]\n ]\n # Sort by started_at descending with tolerant handling of mixed tz-aware/naive values.\n def sort_key(task: Task) -> float:\n started_at = task.started_at\n if started_at is None:\n return float(\"-inf\")\n if not isinstance(started_at, datetime):\n return float(\"-inf\")\n if started_at.tzinfo is None:\n return started_at.replace(tzinfo=UTC).timestamp()\n return started_at.timestamp()\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n # #endregion get_tasks\n # #region get_task_logs [TYPE Function] [C:3]\n # @PURPOSE: Retrieves logs for a specific task (from memory for running, persistence for completed).\n # @PRE: task_id is a string.\n # @POST: Returns list of LogEntry or TaskLog objects.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: log_filter (Optional[LogFilter]) - Filter parameters.\n # @RETURN: List[LogEntry] - List of log entries.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_logs]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None\n ) -> list[LogEntry]:\n with belief_scope(\"TaskManager.get_task_logs\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n # For completed tasks, fetch from persistence\n if task and task.status in [TaskStatus.SUCCESS, TaskStatus.FAILED]:\n if log_filter is None:\n log_filter = LogFilter()\n task_logs = self.log_persistence_service.get_logs(task_id, log_filter)\n # Convert TaskLog to LogEntry for backward compatibility\n return [\n LogEntry(\n timestamp=log.timestamp,\n level=log.level,\n message=log.message,\n source=log.source,\n metadata=log.metadata,\n )\n for log in task_logs\n ]\n # For running/pending tasks, return from memory\n return task.logs if task else []\n # #endregion get_task_logs\n # #region get_task_log_stats [TYPE Function] [C:3]\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_log_stats]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def get_task_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\"TaskManager.get_task_log_stats\", f\"task_id={task_id}\"):\n return self.log_persistence_service.get_log_stats(task_id)\n # #endregion get_task_log_stats\n # #region get_task_log_sources [TYPE Function] [C:3]\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @RELATION: [CALLS] ->[TaskLogPersistenceService.get_sources]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def get_task_log_sources(self, task_id: str) -> list[str]:\n with belief_scope(\"TaskManager.get_task_log_sources\", f\"task_id={task_id}\"):\n return self.log_persistence_service.get_sources(task_id)\n # #endregion get_task_log_sources\n # #region _add_log [TYPE Function] [C:3]\n # @PURPOSE: Adds a log entry to a task buffer and notifies subscribers.\n # @PRE: Task exists.\n # @POST: Log added to buffer and pushed to queues (if level meets task_log_level filter).\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: level (str) - Log level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (str) - Source component (default: \"system\").\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @PARAM: context (Optional[Dict]) - Legacy context (for backward compatibility).\n # @RELATION: [CALLS] ->[should_log_task_level]\n # @RELATION: [DISPATCHES] ->[EventBus]\n def _add_log(\n self,\n task_id: str,\n level: str,\n message: str,\n source: str = \"system\",\n metadata: dict[str, Any] | None = None,\n context: dict[str, Any] | None = None,\n ):\n with belief_scope(\"TaskManager._add_log\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n return\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n # Create log entry with new fields\n log_entry = LogEntry(\n level=level,\n message=message,\n source=source,\n metadata=metadata,\n context=context, # Keep for backward compatibility\n )\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n task.logs.append(log_entry)\n # Add to buffer for batch persistence\n with self._log_buffer_lock:\n if task_id not in self._log_buffer:\n self._log_buffer[task_id] = []\n self._log_buffer[task_id].append(log_entry)\n # Notify subscribers (for real-time WebSocket updates)\n if task_id in self.subscribers:\n for queue in self.subscribers[task_id]:\n self.loop.call_soon_threadsafe(queue.put_nowait, log_entry)\n # #endregion _add_log\n # #region subscribe_logs [TYPE Function] [C:3]\n # @PURPOSE: Subscribes to real-time logs for a task.\n # @PRE: task_id is a string.\n # @POST: Returns an asyncio.Queue for log entries.\n # @PARAM: task_id (str) - ID of the task.\n # @RETURN: asyncio.Queue - Queue for log entries.\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n with belief_scope(\"TaskManager.subscribe_logs\", f\"task_id={task_id}\"):\n queue = asyncio.Queue()\n if task_id not in self.subscribers:\n self.subscribers[task_id] = []\n self.subscribers[task_id].append(queue)\n return queue\n # #endregion subscribe_logs\n # #region unsubscribe_logs [TYPE Function] [C:3]\n # @PURPOSE: Unsubscribes from real-time logs for a task.\n # @PRE: task_id is a string, queue is asyncio.Queue.\n # @POST: Queue removed from subscribers.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: queue (asyncio.Queue) - Queue to remove.\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n with belief_scope(\"TaskManager.unsubscribe_logs\", f\"task_id={task_id}\"):\n if task_id in self.subscribers:\n if queue in self.subscribers[task_id]:\n self.subscribers[task_id].remove(queue)\n if not self.subscribers[task_id]:\n del self.subscribers[task_id]\n # #endregion unsubscribe_logs\n # #region load_persisted_tasks [TYPE Function] [C:3]\n # @PURPOSE: Load persisted tasks using persistence service.\n # @PRE: None.\n # @POST: Persisted tasks loaded into self.tasks.\n # @RELATION: [CALLS] ->[TaskPersistenceService.load_tasks]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n def load_persisted_tasks(self) -> None:\n with belief_scope(\"TaskManager.load_persisted_tasks\"):\n loaded_tasks = self.persistence_service.load_tasks(limit=100)\n for task in loaded_tasks:\n if task.id not in self.tasks:\n self.tasks[task.id] = task\n # #endregion load_persisted_tasks\n # #region await_input [TYPE Function] [C:3]\n # @PURPOSE: Transition a task to AWAITING_INPUT state with input request.\n # @PRE: Task exists and is in RUNNING state.\n # @POST: Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: input_request (Dict) - Details about required input.\n # @THROWS: ValueError if task not found or not RUNNING.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n def await_input(self, task_id: str, input_request: dict[str, Any]) -> None:\n with belief_scope(\"TaskManager.await_input\", f\"task_id={task_id}\"):\n task = self.tasks.get(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n self._add_log(\n task_id,\n \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n # #region resume_task_with_password [TYPE Function] [C:3]\n # @PURPOSE: Resume a task that is awaiting input with provided passwords.\n # @PRE: Task exists and is in AWAITING_INPUT state.\n # @POST: Task status changed to RUNNING, passwords injected, task resumed.\n # @PARAM: task_id (str) - ID of the task.\n # @PARAM: passwords (Dict[str, str]) - Mapping of database name to password.\n # @THROWS: ValueError if task not found, not awaiting input, or passwords invalid.\n # @RELATION: [CALLS] ->[TaskPersistenceService.persist_task]\n # @RELATION: [DEPENDS_ON] ->[JobLifecycle]\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str]\n ) -> None:\n with belief_scope(\n \"TaskManager.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.tasks.get(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self._add_log(\n task_id,\n \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n # #endregion resume_task_with_password\n # #region clear_tasks [TYPE Function] [C:3]\n # @PURPOSE: Clears tasks based on status filter (also deletes associated logs).\n # @PRE: status is Optional[TaskStatus].\n # @POST: Tasks matching filter (or all non-active) cleared from registry and database.\n # @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n # @RETURN: int - Number of tasks cleared.\n # @RELATION: [CALLS] ->[TaskPersistenceService.delete_tasks]\n # @RELATION: [DEPENDS_ON] ->[TaskGraph]\n # @RELATION: [DEPENDS_ON] ->[EventBus]\n def clear_tasks(self, status: TaskStatus | None = None) -> int:\n with belief_scope(\"TaskManager.clear_tasks\"):\n tasks_to_remove = []\n for task_id, task in list(self.tasks.items()):\n # If status is provided, match it.\n # If status is None, match everything EXCEPT RUNNING (unless they are awaiting input/mapping which are technically running but paused?)\n # Actually, AWAITING_INPUT and AWAITING_MAPPING are distinct statuses in TaskStatus enum.\n # RUNNING is active execution.\n should_remove = False\n if status:\n if task.status == status:\n should_remove = True\n else:\n # Clear all non-active tasks (keep RUNNING, AWAITING_INPUT, AWAITING_MAPPING)\n if task.status not in [\n TaskStatus.RUNNING,\n TaskStatus.AWAITING_INPUT,\n TaskStatus.AWAITING_MAPPING,\n ]:\n should_remove = True\n if should_remove:\n tasks_to_remove.append(task_id)\n for tid in tasks_to_remove:\n # Cancel future if exists (e.g. for AWAITING_INPUT/MAPPING)\n if tid in self.task_futures:\n self.task_futures[tid].cancel()\n del self.task_futures[tid]\n del self.tasks[tid]\n # Remove from persistence (task_records and task_logs via CASCADE)\n self.persistence_service.delete_tasks(tasks_to_remove)\n # Also explicitly delete logs (in case CASCADE is not set up)\n if tasks_to_remove:\n self.log_persistence_service.delete_logs_for_tasks(tasks_to_remove)\n logger.info(f\"Cleared {len(tasks_to_remove)} tasks.\")\n return len(tasks_to_remove)\n # #endregion clear_tasks\n# #endregion TaskManager\n" + "body": "# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state]\n# @BRIEF Facade composing TaskGraph, EventBus, and JobLifecycle into a single interface.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @PRE: Plugin loader resolves plugin ids and persistence services are available.\n# @POST: In-memory task graph, lifecycle scheduler, and log event bus stay consistent with\n# persisted task state.\n# @INVARIANT: Task IDs are unique within the registry.\n# @INVARIANT: Each task has exactly one status at any time.\n# @INVARIANT: Log entries are never deleted after being added to a task.\n# @SIDE_EFFECT: Spawns worker threads, flushes logs to database, and mutates task states.\n# @DATA_CONTRACT: Input[plugin_id, params] -> Output[Task]\n# @RATIONALE Thin facade — all delegating methods are under 15 lines. Actual business logic\n# (registry CRUD, log buffering, lifecycle state machine) lives in extracted modules.\n# @REJECTED Keeping all five concerns in one class was rejected — it violated INV_7 (708-line\n# manager.py) and made the god class impossible to test or maintain independently.\nclass TaskManager:\n \"\"\"\n Facade composing TaskGraph (registry), EventBus (log/pub-sub), and\n JobLifecycle (state machine) into a single TaskManager interface.\n \"\"\"\n\n # #region __init__ [TYPE Function] [C:5]\n # @BRIEF Initialize sub-services, create add_log callback, start background flusher.\n # @PRE: plugin_loader is initialized.\n # @POST: TaskManager is ready to accept tasks.\n # @SIDE_EFFECT: Starts background flusher thread and loads persisted task state into memory.\n def __init__(self, plugin_loader):\n with belief_scope(\"TaskManager.__init__\"):\n logger.reason(\"Initializing task manager runtime services\")\n self.plugin_loader = plugin_loader\n self.executor = ThreadPoolExecutor(max_workers=5)\n self.persistence_service = TaskPersistenceService()\n self.log_persistence_service = TaskLogPersistenceService()\n\n try:\n self.loop = asyncio.get_running_loop()\n except RuntimeError:\n self.loop = asyncio.get_event_loop()\n\n # Create sub-services\n self.graph = TaskGraph(self.persistence_service)\n self.event_bus = EventBus(self.log_persistence_service, self.loop)\n\n # Create add_log callback that lifecycle and plugins use\n self._add_log = self._make_add_log_callback()\n\n self.lifecycle = JobLifecycle(\n plugin_loader=self.plugin_loader,\n graph=self.graph,\n event_bus=self.event_bus,\n persistence_service=self.persistence_service,\n executor=self.executor,\n loop=self.loop,\n )\n\n # Load persisted tasks on startup\n self.graph.load_persisted_tasks()\n\n # Backward-compatible property aliases for tests\n self.tasks = self.graph.tasks\n self.task_futures = self.graph.task_futures\n self.subscribers = self.event_bus.subscribers\n self._log_buffer = self.event_bus._log_buffer\n self._log_buffer_lock = self.event_bus._log_buffer_lock\n self._flusher_stop_event = self.event_bus._flusher_stop_event\n self._flusher_thread = self.event_bus._flusher_thread\n\n logger.reflect(\n \"Task manager runtime initialized\",\n extra={\"task_count\": len(self.tasks)},\n )\n # #endregion __init__\n\n # #region _make_add_log_callback [C:2] [TYPE Function]\n # @BRIEF Create a closure for adding logs that looks up the task and delegates to EventBus.\n def _make_add_log_callback(self):\n def _add_log(task_id, level, message, source=\"system\", metadata=None, context=None):\n task = self.graph.get_task(task_id)\n if not task:\n return\n self.event_bus.add_log(\n task_id, level, message, source, metadata, context,\n task_logs_list=task.logs,\n )\n return _add_log\n # #endregion _make_add_log_callback\n\n # ── Deprecated legacy aliases (delegate to EventBus) ──\n\n # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Legacy alias delegating to EventBus._flusher_loop.\n def _flusher_loop(self):\n self.event_bus._flusher_loop()\n # #endregion _flusher_loop\n\n # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Legacy alias delegating to EventBus._flush_logs.\n def _flush_logs(self):\n self.event_bus._flush_logs()\n # #endregion _flush_logs\n\n # #region _flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Legacy alias delegating to EventBus.flush_task_logs.\n def _flush_task_logs(self, task_id: str):\n self.event_bus.flush_task_logs(task_id)\n # #endregion _flush_task_logs\n\n # ── Task CRUD delegates to TaskGraph ──\n\n # #region get_task [TYPE Function] [C:2]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.graph.get_task(task_id)\n # #endregion get_task\n\n # #region get_all_tasks [TYPE Function] [C:1]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return self.graph.get_all_tasks()\n # #endregion get_all_tasks\n\n # #region get_tasks [TYPE Function] [C:3]\n # @BRIEF Retrieves tasks with pagination and optional status filter.\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n return self.graph.get_tasks(limit, offset, status, plugin_ids, completed_only)\n # #endregion get_tasks\n\n # #region load_persisted_tasks [TYPE Function] [C:2]\n # @BRIEF Load persisted tasks using persistence service.\n def load_persisted_tasks(self) -> None:\n self.graph.load_persisted_tasks(limit=100)\n # #endregion load_persisted_tasks\n\n # #region clear_tasks [TYPE Function] [C:4]\n # @BRIEF Clears tasks based on status filter (also deletes associated logs).\n # @SIDE_EFFECT Removes tasks from registry and persistence; cancels futures.\n def clear_tasks(self, status: TaskStatus | None = None) -> int:\n with belief_scope(\"TaskManager.clear_tasks\"):\n tasks_to_remove = []\n for task_id, task in list(self.tasks.items()):\n should_remove = False\n if status:\n if task.status == status:\n should_remove = True\n else:\n if task.status not in [\n TaskStatus.RUNNING,\n TaskStatus.AWAITING_INPUT,\n TaskStatus.AWAITING_MAPPING,\n ]:\n should_remove = True\n if should_remove:\n tasks_to_remove.append(task_id)\n\n # Delete logs first, then remove tasks\n if tasks_to_remove:\n self.event_bus.delete_logs_for_tasks(tasks_to_remove)\n removed = self.graph.remove_tasks(tasks_to_remove)\n logger.info(f\"Cleared {removed} tasks.\")\n return removed\n # #endregion clear_tasks\n\n # ── Log delegates to EventBus ──\n\n # #region get_task_logs [TYPE Function] [C:3]\n # @BRIEF Retrieves logs for a specific task (from memory or persistence).\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None\n ) -> list:\n task = self.graph.get_task(task_id)\n task_status = task.status if task else None\n task_logs = task.logs if task else []\n return self.event_bus.get_task_logs(\n task_id, log_filter, task_status=task_status, task_logs=task_logs\n )\n # #endregion get_task_logs\n\n # #region get_task_log_stats [TYPE Function] [C:2]\n # @BRIEF Get statistics about logs for a task.\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.event_bus.get_task_log_stats(task_id)\n # #endregion get_task_log_stats\n\n # #region get_task_log_sources [TYPE Function] [C:2]\n # @BRIEF Get unique sources for a task's logs.\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.event_bus.get_task_log_sources(task_id)\n # #endregion get_task_log_sources\n\n # ── Subscription delegates to EventBus ──\n\n # #region subscribe_logs [TYPE Function] [C:2]\n # @BRIEF Subscribes to real-time logs for a task.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n return await self.event_bus.subscribe_logs(task_id)\n # #endregion subscribe_logs\n\n # #region unsubscribe_logs [TYPE Function] [C:2]\n # @BRIEF Unsubscribes from real-time logs for a task.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n self.event_bus.unsubscribe_logs(task_id, queue)\n # #endregion unsubscribe_logs\n\n # ── Lifecycle delegates to JobLifecycle ──\n\n # #region create_task [TYPE Function] [C:4]\n # @BRIEF Creates and queues a new task for execution.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None\n ) -> Task:\n return await self.lifecycle.create_task(\n plugin_id, params, user_id,\n add_log_callback=self._add_log,\n )\n # #endregion create_task\n\n # #region _run_task [TYPE Function] [C:4]\n # @BRIEF Internal method to execute a task with TaskContext support (delegates to lifecycle).\n async def _run_task(self, task_id: str):\n await self.lifecycle._run_task(task_id, add_log_callback=self._add_log)\n # #endregion _run_task\n\n # #region resolve_task [TYPE Function] [C:3]\n # @BRIEF Resumes a task that is awaiting mapping.\n async def resolve_task(self, task_id: str, resolution_params: dict[str, Any]):\n await self.lifecycle.resolve_task(task_id, resolution_params)\n # #endregion resolve_task\n\n # #region wait_for_resolution [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for a resolution signal.\n async def wait_for_resolution(self, task_id: str):\n await self.lifecycle.wait_for_resolution(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for user input.\n async def wait_for_input(self, task_id: str):\n await self.lifecycle.wait_for_input(task_id)\n # #endregion wait_for_input\n\n # #region await_input [TYPE Function] [C:3]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n def await_input(self, task_id: str, input_request: dict[str, Any]) -> None:\n self.lifecycle.await_input(\n task_id, input_request,\n add_log_callback=self._add_log,\n )\n # #endregion await_input\n\n # #region resume_task_with_password [TYPE Function] [C:3]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str]\n ) -> None:\n self.lifecycle.resume_task_with_password(\n task_id, passwords,\n add_log_callback=self._add_log,\n )\n # #endregion resume_task_with_password\n# #endregion TaskManager\n" + }, + { + "contract_id": "_make_add_log_callback", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/manager.py", + "start_line": 128, + "end_line": 140, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Create a closure for adding logs that looks up the task and delegates to EventBus." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _make_add_log_callback [C:2] [TYPE Function]\n # @BRIEF Create a closure for adding logs that looks up the task and delegates to EventBus.\n def _make_add_log_callback(self):\n def _add_log(task_id, level, message, source=\"system\", metadata=None, context=None):\n task = self.graph.get_task(task_id)\n if not task:\n return\n self.event_bus.add_log(\n task_id, level, message, source, metadata, context,\n task_logs_list=task.logs,\n )\n return _add_log\n # #endregion _make_add_log_callback\n" + }, + { + "contract_id": "_flusher_loop", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/manager.py", + "start_line": 144, + "end_line": 148, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Legacy alias delegating to EventBus._flusher_loop." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Legacy alias delegating to EventBus._flusher_loop.\n def _flusher_loop(self):\n self.event_bus._flusher_loop()\n # #endregion _flusher_loop\n" + }, + { + "contract_id": "_flush_logs", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/manager.py", + "start_line": 150, + "end_line": 154, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Legacy alias delegating to EventBus._flush_logs." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Legacy alias delegating to EventBus._flush_logs.\n def _flush_logs(self):\n self.event_bus._flush_logs()\n # #endregion _flush_logs\n" + }, + { + "contract_id": "_flush_task_logs", + "contract_type": "Function", + "file_path": "backend/src/core/task_manager/manager.py", + "start_line": 156, + "end_line": 160, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Legacy alias delegating to EventBus.flush_task_logs." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Legacy alias delegating to EventBus.flush_task_logs.\n def _flush_task_logs(self, task_id: str):\n self.event_bus.flush_task_logs(task_id)\n # #endregion _flush_task_logs\n" }, { "contract_id": "TaskManagerModels", "contract_type": "Module", "file_path": "backend/src/core/task_manager/models.py", "start_line": 1, - "end_line": 112, - "tier": "TIER_2", - "complexity": 3, + "end_line": 116, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "CONSTRAINT": "Must use Pydantic for data validation.", + "DATA_CONTRACT": "TaskInput -> TaskModel", "INVARIANT": "Task IDs are immutable once created.", - "LAYER": "Core", - "PURPOSE": "Defines the data models and enumerations used by the Task Manager." + "LAYER": "Domain", + "POST": "Task models exported with immutable IDs", + "PRE": "Task manager initialized", + "PURPOSE": "Defines the data models and enumerations used by the Task Manager.", + "SIDE_EFFECT": "Defines task data schema" }, "relations": [ { @@ -37170,15 +38013,6 @@ "message": "@CONSTRAINT is not defined in axiom_config.yaml tags", "detail": null }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -37218,14 +38052,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskManagerModels [C:3] [TYPE Module] [SEMANTICS pydantic, task, model, validate, task-status]\n# @BRIEF Defines the data models and enumerations used by the Task Manager.\n# @LAYER: Core\n# @RELATION USED_BY -> [TaskManager]\n# @RELATION USED_BY -> [TaskManagerPackage]\n# @INVARIANT: Task IDs are immutable once created.\n# @CONSTRAINT: Must use Pydantic for data validation.\nimport uuid\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region TaskStatus [TYPE Enum]\nclass TaskStatus(str, Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n SUCCESS = \"SUCCESS\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n AWAITING_INPUT = \"AWAITING_INPUT\"\n# #endregion TaskStatus\n# #region LogLevel [TYPE Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n# #endregion LogLevel\n# #region LogEntry [C:2] [TYPE Class]\n# @BRIEF A Pydantic model representing a single, structured log entry associated with a task.\n#\n# {\n# required_fields: {message: str},\n# optional_fields: {timestamp: datetime, level: str, source: str, context: dict, metadata: dict}\n# }\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str = Field(default=\"INFO\")\n message: str\n source: str = Field(\n default=\"system\"\n ) # Component attribution: plugin, superset_api, git, etc.\n context: dict[str, Any] | None = (\n None # Legacy field, kept for backward compatibility\n )\n metadata: dict[str, Any] | None = (\n None # Structured metadata (e.g., dashboard_id, progress)\n )\n# #endregion LogEntry\n# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n class Config:\n from_attributes = True\n# #endregion TaskLog\n# #region LogFilter [TYPE Class]\nclass LogFilter(BaseModel):\n level: str | None = None # Filter by log level\n source: str | None = None # Filter by source component\n search: str | None = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n# #endregion LogFilter\n# #region LogStats [C:1] [TYPE Class] [SEMANTICS log, stats, aggregation, pydantic]\n# @BRIEF Statistics about log entries for a task.\n# @RELATION DEPENDS_ON -> [TaskLog]\nclass LogStats(BaseModel):\n total_count: int\n by_level: dict[str, int] # {\"INFO\": 10, \"ERROR\": 2}\n by_source: dict[str, int] # {\"plugin\": 5, \"superset_api\": 7}\n# #endregion LogStats\n# #region Task [C:3] [TYPE Class] [SEMANTICS task, job, execution, state, pydantic]\n# @BRIEF A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs.\n# @RELATION DEPENDS_ON -> [TaskStatus]\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskManager]\nclass Task(BaseModel):\n id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n plugin_id: str\n status: TaskStatus = TaskStatus.PENDING\n started_at: datetime | None = None\n finished_at: datetime | None = None\n user_id: str | None = None\n logs: list[LogEntry] = Field(default_factory=list)\n params: dict[str, Any] = Field(default_factory=dict)\n input_required: bool = False\n input_request: dict[str, Any] | None = None\n # Result payload can be dict/list/scalar depending on plugin and legacy records.\n result: Any | None = None\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the Task model and validates input_request for AWAITING_INPUT status.\n # @PRE: If status is AWAITING_INPUT, input_request must be provided.\n # @POST: Task instance is created or ValueError is raised.\n # @PARAM: **data - Keyword arguments for model initialization.\n def __init__(self, **data):\n super().__init__(**data)\n if self.status == TaskStatus.AWAITING_INPUT and not self.input_request:\n raise ValueError(\"input_request is required when status is AWAITING_INPUT\")\n # #endregion __init__\n# #endregion Task\n# #endregion TaskManagerModels\n" + "body": "# #region TaskManagerModels [C:5] [TYPE Module] [SEMANTICS pydantic, task, model, validate, task-status]\n# @BRIEF Defines the data models and enumerations used by the Task Manager.\n# @LAYER: Domain\n# @RELATION USED_BY -> [TaskManager]\n# @RELATION USED_BY -> [TaskManagerPackage]\n# @INVARIANT: Task IDs are immutable once created.\n# @CONSTRAINT: Must use Pydantic for data validation.\n# @PRE: Task manager initialized\n# @POST: Task models exported with immutable IDs\n# @SIDE_EFFECT: Defines task data schema\n# @DATA_CONTRACT: TaskInput -> TaskModel\nimport uuid\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region TaskStatus [TYPE Enum]\nclass TaskStatus(str, Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n SUCCESS = \"SUCCESS\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n AWAITING_INPUT = \"AWAITING_INPUT\"\n# #endregion TaskStatus\n# #region LogLevel [TYPE Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n# #endregion LogLevel\n# #region LogEntry [C:2] [TYPE Class]\n# @BRIEF A Pydantic model representing a single, structured log entry associated with a task.\n#\n# {\n# required_fields: {message: str},\n# optional_fields: {timestamp: datetime, level: str, source: str, context: dict, metadata: dict}\n# }\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str = Field(default=\"INFO\")\n message: str\n source: str = Field(\n default=\"system\"\n ) # Component attribution: plugin, superset_api, git, etc.\n context: dict[str, Any] | None = (\n None # Legacy field, kept for backward compatibility\n )\n metadata: dict[str, Any] | None = (\n None # Structured metadata (e.g., dashboard_id, progress)\n )\n# #endregion LogEntry\n# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n class Config:\n from_attributes = True\n# #endregion TaskLog\n# #region LogFilter [TYPE Class]\nclass LogFilter(BaseModel):\n level: str | None = None # Filter by log level\n source: str | None = None # Filter by source component\n search: str | None = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n# #endregion LogFilter\n# #region LogStats [C:1] [TYPE Class] [SEMANTICS log, stats, aggregation, pydantic]\n# @BRIEF Statistics about log entries for a task.\n# @RELATION DEPENDS_ON -> [TaskLog]\nclass LogStats(BaseModel):\n total_count: int\n by_level: dict[str, int] # {\"INFO\": 10, \"ERROR\": 2}\n by_source: dict[str, int] # {\"plugin\": 5, \"superset_api\": 7}\n# #endregion LogStats\n# #region Task [C:3] [TYPE Class] [SEMANTICS task, job, execution, state, pydantic]\n# @BRIEF A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs.\n# @RELATION DEPENDS_ON -> [TaskStatus]\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskManager]\nclass Task(BaseModel):\n id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n plugin_id: str\n status: TaskStatus = TaskStatus.PENDING\n started_at: datetime | None = None\n finished_at: datetime | None = None\n user_id: str | None = None\n logs: list[LogEntry] = Field(default_factory=list)\n params: dict[str, Any] = Field(default_factory=dict)\n input_required: bool = False\n input_request: dict[str, Any] | None = None\n # Result payload can be dict/list/scalar depending on plugin and legacy records.\n result: Any | None = None\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the Task model and validates input_request for AWAITING_INPUT status.\n # @PRE: If status is AWAITING_INPUT, input_request must be provided.\n # @POST: Task instance is created or ValueError is raised.\n # @PARAM: **data - Keyword arguments for model initialization.\n def __init__(self, **data):\n super().__init__(**data)\n if self.status == TaskStatus.AWAITING_INPUT and not self.input_request:\n raise ValueError(\"input_request is required when status is AWAITING_INPUT\")\n # #endregion __init__\n# #endregion Task\n# #endregion TaskManagerModels\n" }, { "contract_id": "TaskStatus", "contract_type": "Enum", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 16, - "end_line": 24, + "start_line": 20, + "end_line": 28, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -37239,8 +38073,8 @@ "contract_id": "LogLevel", "contract_type": "Enum", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 25, - "end_line": 31, + "start_line": 29, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -37254,8 +38088,8 @@ "contract_id": "TaskLog", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 53, - "end_line": 66, + "start_line": 57, + "end_line": 70, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -37279,8 +38113,8 @@ "contract_id": "LogFilter", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 67, - "end_line": 74, + "start_line": 71, + "end_line": 78, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -37294,8 +38128,8 @@ "contract_id": "LogStats", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 75, - "end_line": 82, + "start_line": 79, + "end_line": 86, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37329,8 +38163,8 @@ "contract_id": "Task", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 83, - "end_line": 111, + "start_line": 87, + "end_line": 115, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -38512,10 +39346,19 @@ "detail": null }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TIER", - "message": "@TIER is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TIER value 'STANDARD' is not in allowed enum", + "detail": { + "allowed": [ + "TIER_1", + "TIER_2", + "TIER_3", + "TIER_4", + "TIER_5" + ], + "value": "STANDARD" + } }, { "code": "tag_forbidden_by_complexity", @@ -39258,7 +40101,7 @@ "contract_type": "Module", "file_path": "backend/src/core/utils/network.py", "start_line": 1, - "end_line": 538, + "end_line": 543, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -39302,7 +40145,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region NetworkModule [C:3] [TYPE Module] [SEMANTICS network, http, retry, tenacity, session]\n#\n# @BRIEF Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PUBLIC_API: APIClient\nimport io\nimport json\nimport threading\nimport time\nfrom pathlib import Path\nfrom typing import Any, cast\n\nimport requests\nimport urllib3\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\n\n# #region SupersetAPIError [C:1] [TYPE Class]\n# @BRIEF Base exception for all Superset API related errors.\nclass SupersetAPIError(Exception):\n # #region __init__ [TYPE Function] [C:1]\n # @PURPOSE: Initializes the exception with a message and context.\n # @PRE: message is a string, context is a dict.\n # @POST: Exception is initialized with context.\n def __init__(self, message: str = \"Superset API error\", **context: Any):\n with belief_scope(\"SupersetAPIError.__init__\"):\n self.context = context\n super().__init__(f\"[API_FAILURE] {message} | Context: {self.context}\")\n # #endregion __init__\n# #endregion SupersetAPIError\n# #region AuthenticationError [C:1] [TYPE Class]\n# @BRIEF Exception raised when authentication fails.\nclass AuthenticationError(SupersetAPIError):\n # #region __init__ [TYPE Function] [C:1]\n # @PURPOSE: Initializes the authentication error.\n # @PRE: message is a string, context is a dict.\n # @POST: AuthenticationError is initialized.\n def __init__(self, message: str = \"Authentication failed\", **context: Any):\n with belief_scope(\"AuthenticationError.__init__\"):\n super().__init__(message, type=\"authentication\", **context)\n # #endregion __init__\n# #endregion AuthenticationError\n# #region PermissionDeniedError [TYPE Class]\n# @BRIEF Exception raised when access is denied.\nclass PermissionDeniedError(AuthenticationError):\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the permission denied error.\n # @PRE: message is a string, context is a dict.\n # @POST: PermissionDeniedError is initialized.\n def __init__(self, message: str = \"Permission denied\", **context: Any):\n with belief_scope(\"PermissionDeniedError.__init__\"):\n super().__init__(message, **context)\n # #endregion __init__\n# #endregion PermissionDeniedError\n# #region DashboardNotFoundError [TYPE Class]\n# @BRIEF Exception raised when a dashboard cannot be found.\nclass DashboardNotFoundError(SupersetAPIError):\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the not found error with resource ID.\n # @PRE: resource_id is provided.\n # @POST: DashboardNotFoundError is initialized.\n def __init__(self, resource_id: int | str, message: str = \"Dashboard not found\", **context: Any):\n with belief_scope(\"DashboardNotFoundError.__init__\"):\n super().__init__(f\"Dashboard '{resource_id}' {message}\", subtype=\"not_found\", resource_id=resource_id, **context)\n # #endregion __init__\n# #endregion DashboardNotFoundError\n# #region NetworkError [TYPE Class]\n# @BRIEF Exception raised when a network level error occurs.\nclass NetworkError(Exception):\n # #region NetworkError.__init__ [TYPE Function]\n # @PURPOSE: Initializes the network error.\n # @PRE: message is a string.\n # @POST: NetworkError is initialized.\n def __init__(self, message: str = \"Network connection failed\", **context: Any):\n with belief_scope(\"NetworkError.__init__\"):\n self.context = context\n super().__init__(f\"[NETWORK_FAILURE] {message} | Context: {self.context}\")\n # #endregion NetworkError.__init__\n# #endregion NetworkError\n# #region SupersetAuthCache [TYPE Class]\n# @BRIEF Process-local cache for Superset access/csrf tokens keyed by environment credentials.\n# @PRE: base_url and username are stable strings.\n# @POST: Cached entries expire automatically by TTL and can be reused across requests.\nclass SupersetAuthCache:\n TTL_SECONDS = 300\n _lock = threading.Lock()\n _entries: dict[tuple[str, str, bool], dict[str, Any]] = {}\n @classmethod\n def build_key(cls, base_url: str, auth: dict[str, Any] | None, verify_ssl: bool) -> tuple[str, str, bool]:\n username = \"\"\n if isinstance(auth, dict):\n username = str(auth.get(\"username\") or \"\").strip()\n return (str(base_url or \"\").strip(), username, bool(verify_ssl))\n @classmethod\n # #region SupersetAuthCache.get [TYPE Function]\n def get(cls, key: tuple[str, str, bool]) -> dict[str, str] | None:\n now = time.time()\n with cls._lock:\n payload = cls._entries.get(key)\n if not payload:\n return None\n expires_at = float(payload.get(\"expires_at\") or 0)\n if expires_at <= now:\n cls._entries.pop(key, None)\n return None\n tokens = payload.get(\"tokens\")\n if not isinstance(tokens, dict):\n cls._entries.pop(key, None)\n return None\n return {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n }\n # #endregion SupersetAuthCache.get\n @classmethod\n # #region SupersetAuthCache.set [TYPE Function]\n def set(cls, key: tuple[str, str, bool], tokens: dict[str, str], ttl_seconds: int | None = None) -> None:\n normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)\n with cls._lock:\n cls._entries[key] = {\n \"tokens\": {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n },\n \"expires_at\": time.time() + normalized_ttl,\n }\n # #endregion SupersetAuthCache.set\n @classmethod\n def invalidate(cls, key: tuple[str, str, bool]) -> None:\n with cls._lock:\n cls._entries.pop(key, None)\n# #endregion SupersetAuthCache\n# #region APIClient [C:3] [TYPE Class]\n# @BRIEF Synchronous Superset API client with process-local auth token caching.\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @RELATION DEPENDS_ON -> [LoggerModule]\nclass APIClient:\n DEFAULT_TIMEOUT = 30\n # #region APIClient.__init__ [TYPE Function]\n # @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером.\n # @PARAM: config (Dict[str, Any]) - Конфигурация.\n # @PARAM: verify_ssl (bool) - Проверять ли SSL.\n # @PARAM: timeout (int) - Таймаут запросов.\n # @PRE: config must contain 'base_url' and 'auth'.\n # @POST: APIClient instance is initialized with a session.\n def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\n with belief_scope(\"__init__\"):\n app_logger.reason(\"Initializing APIClient.\", extra={\"src\": \"APIClient.__init__\"})\n self.base_url: str = self._normalize_base_url(config.get(\"base_url\", \"\"))\n self.api_base_url: str = f\"{self.base_url}/api/v1\"\n self.auth = config.get(\"auth\")\n self.request_settings = {\"verify_ssl\": verify_ssl, \"timeout\": timeout}\n self.session = self._init_session()\n self._tokens: dict[str, str] = {}\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n self._authenticated = False\n app_logger.reflect(\"APIClient initialized.\", extra={\"src\": \"APIClient.__init__\"})\n # #endregion APIClient.__init__\n # #region _init_session [TYPE Function]\n # @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой.\n # @PRE: self.request_settings must be initialized.\n # @POST: Returns a configured requests.Session instance.\n # @RETURN: requests.Session - Настроенная сессия.\n def _init_session(self) -> requests.Session:\n with belief_scope(\"_init_session\"):\n session = requests.Session()\n\n # Create a custom adapter that handles TLS issues\n class TLSAdapter(HTTPAdapter):\n def init_poolmanager(self, connections, maxsize, block=False):\n import ssl\n\n from urllib3.poolmanager import PoolManager\n\n # Create an SSL context that ignores TLSv1 unrecognized name errors\n ctx = ssl.create_default_context()\n ctx.set_ciphers('HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')\n\n # Ignore TLSV1_UNRECOGNIZED_NAME errors by disabling hostname verification\n # This is safe when verify_ssl is false (we're already not verifying the certificate)\n ctx.check_hostname = False\n\n self.poolmanager = PoolManager(\n num_pools=connections,\n maxsize=maxsize,\n block=block,\n ssl_context=ctx\n )\n\n retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])\n adapter = TLSAdapter(max_retries=retries)\n session.mount('http://', adapter)\n session.mount('https://', adapter)\n\n if not self.request_settings[\"verify_ssl\"]:\n urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n app_logger.warning(\"[_init_session][State] SSL verification disabled.\")\n # When verify_ssl is false, we should also disable hostname verification\n session.verify = False\n else:\n session.verify = True\n\n return session\n # #endregion _init_session\n # #region _normalize_base_url [TYPE Function]\n # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.\n # @PRE: raw_url can be empty.\n # @POST: Returns canonical base URL suitable for building API endpoints.\n # @RETURN: str\n def _normalize_base_url(self, raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n return normalized.rstrip(\"/\")\n # #endregion _normalize_base_url\n # #region _build_api_url [TYPE Function]\n # @PURPOSE: Build absolute Superset API URL for endpoint using canonical /api/v1 base.\n # @PRE: endpoint is relative path or absolute URL.\n # @POST: Returns full URL without accidental duplicate slashes.\n # @RETURN: str\n def _build_api_url(self, endpoint: str) -> str:\n normalized_endpoint = str(endpoint or \"\").strip()\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n return normalized_endpoint\n if not normalized_endpoint.startswith(\"/\"):\n normalized_endpoint = f\"/{normalized_endpoint}\"\n if normalized_endpoint.startswith(\"/api/v1/\") or normalized_endpoint == \"/api/v1\":\n return f\"{self.base_url}{normalized_endpoint}\"\n return f\"{self.api_base_url}{normalized_endpoint}\"\n # #endregion _build_api_url\n # #region APIClient.authenticate [TYPE Function]\n # @PURPOSE: Выполняет аутентификацию в Superset API и получает access и CSRF токены.\n # @PRE: self.auth and self.base_url must be valid.\n # @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`.\n # @RETURN: Dict[str, str] - Словарь с токенами.\n # @THROW: AuthenticationError, NetworkError - при ошибках.\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n def authenticate(self) -> dict[str, str]:\n with belief_scope(\"authenticate\"):\n app_logger.info(\"[authenticate][Enter] Authenticating to %s\", self.base_url)\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\"):\n # Cache hit — we have a valid access_token, but we need to\n # re-establish the session cookie in this new requests.Session.\n # Superset CSRF protection pairs the X-CSRFToken header with a\n # Flask session cookie set during login. A fresh requests.Session\n # lacks that cookie, so even a valid cached CSRF token would fail\n # with \"CSRF session token is missing\". We re-fetch the CSRF\n # token to both establish the session cookie AND get a fresh\n # CSRF token for the current session.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {cached_tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n csrf_token = csrf_response.json()[\"result\"]\n self._tokens = {\n \"access_token\": cached_tokens[\"access_token\"],\n \"csrf_token\": csrf_token,\n }\n self._authenticated = True\n # Update cache with fresh CSRF token for this session\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.info(\n \"[authenticate][CacheHit] Reusing cached Superset auth tokens for %s \"\n \"(CSRF token refreshed for new session)\",\n self.base_url,\n )\n return self._tokens\n except Exception as refresh_err:\n app_logger.warning(\n \"[authenticate][CacheRefreshFailed] CSRF token refresh failed, \"\n \"falling back to full re-authentication: %s\",\n refresh_err,\n )\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full authentication below\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n # Log the payload keys and values (masking password)\n masked_auth = {k: (\"******\" if k == \"password\" else v) for k, v in self.auth.items()}\n app_logger.info(f\"[authenticate][Debug] Login URL: {login_url}\")\n app_logger.info(f\"[authenticate][Debug] Auth payload: {masked_auth}\")\n\n response = self.session.post(login_url, json=self.auth, timeout=self.request_settings[\"timeout\"])\n\n if response.status_code != 200:\n app_logger.error(f\"[authenticate][Error] Status: {response.status_code}, Response: {response.text}\")\n\n response.raise_for_status()\n access_token = response.json()[\"access_token\"]\n\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(csrf_url, headers={\"Authorization\": f\"Bearer {access_token}\"}, timeout=self.request_settings[\"timeout\"])\n csrf_response.raise_for_status()\n\n self._tokens = {\"access_token\": access_token, \"csrf_token\": csrf_response.json()[\"result\"]}\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.reflect(\"Authenticated successfully.\", extra={\"src\": \"authenticate\"})\n return self._tokens\n except requests.exceptions.HTTPError as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = e.response.status_code if e.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable during authentication (Status {status_code})\", status_code=status_code) from e\n raise AuthenticationError(f\"Authentication failed: {e}\") from e\n except (requests.exceptions.RequestException, KeyError) as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {e}\") from e\n # #endregion APIClient.authenticate\n @property\n # #region headers [TYPE Function]\n # @PURPOSE: Возвращает HTTP-заголовки для аутентифицированных запросов.\n # @PRE: APIClient is initialized and authenticated or can be authenticated.\n # @POST: Returns headers including auth tokens.\n def headers(self) -> dict[str, str]:\n if not self._authenticated:\n self.authenticate()\n return {\n \"Authorization\": f\"Bearer {self._tokens['access_token']}\",\n \"X-CSRFToken\": self._tokens.get(\"csrf_token\", \"\"),\n \"Referer\": self.base_url,\n \"Content-Type\": \"application/json\"\n }\n # #endregion headers\n # #region request [TYPE Function]\n # @PURPOSE: Выполняет универсальный HTTP-запрос к API.\n # @PARAM: method (str) - HTTP метод.\n # @PARAM: endpoint (str) - API эндпоинт.\n # @PARAM: headers (Optional[Dict]) - Дополнительные заголовки.\n # @PARAM: raw_response (bool) - Возвращать ли сырой ответ.\n # @PRE: method and endpoint must be strings.\n # @POST: Returns response content or raw Response object.\n # @RETURN: `requests.Response` если `raw_response=True`, иначе `dict`.\n # @THROW: SupersetAPIError, NetworkError и их подклассы.\n def request(self, method: str, endpoint: str, headers: dict | None = None, raw_response: bool = False, **kwargs) -> requests.Response | dict[str, Any]:\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n if headers:\n _headers.update(headers)\n\n try:\n response = self.session.request(method, full_url, headers=_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except requests.exceptions.HTTPError as e:\n if e.response is not None and e.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(e, endpoint)\n except requests.exceptions.RequestException as e:\n self._handle_network_error(e, full_url)\n # #endregion request\n # #region _handle_http_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует HTTP ошибки в кастомные исключения.\n # @PARAM: e (requests.exceptions.HTTPError) - Ошибка.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PRE: e must be a valid HTTPError with a response.\n # @POST: Raises a specific SupersetAPIError or subclass.\n def _handle_http_error(self, e: requests.exceptions.HTTPError, endpoint: str):\n with belief_scope(\"_handle_http_error\"):\n status_code = e.response.status_code\n if status_code == 502 or status_code == 503 or status_code == 504:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from e\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from e\n raise SupersetAPIError(\n f\"API resource not found at endpoint '{endpoint}'\",\n status_code=status_code,\n endpoint=endpoint,\n subtype=\"not_found\",\n ) from e\n if status_code == 403:\n raise PermissionDeniedError() from e\n if status_code == 401:\n raise AuthenticationError() from e\n raise SupersetAPIError(f\"API Error {status_code}: {e.response.text}\") from e\n # #endregion _handle_http_error\n # #region _is_dashboard_endpoint [TYPE Function]\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\n # @PRE: endpoint may be relative or absolute.\n # @POST: Returns true only for dashboard-specific endpoints.\n def _is_dashboard_endpoint(self, endpoint: str) -> bool:\n normalized_endpoint = str(endpoint or \"\").strip().lower()\n if not normalized_endpoint:\n return False\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n try:\n normalized_endpoint = \"/\" + normalized_endpoint.split(\"/api/v1\", 1)[1].lstrip(\"/\")\n except IndexError:\n return False\n if normalized_endpoint.startswith(\"/api/v1/\"):\n normalized_endpoint = normalized_endpoint[len(\"/api/v1\"):]\n return normalized_endpoint.startswith(\"/dashboard/\") or normalized_endpoint == \"/dashboard\"\n # #endregion _is_dashboard_endpoint\n # #region _handle_network_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует сетевые ошибки в `NetworkError`.\n # @PARAM: e (requests.exceptions.RequestException) - Ошибка.\n # @PARAM: url (str) - URL.\n # @PRE: e must be a RequestException.\n # @POST: Raises a NetworkError.\n def _handle_network_error(self, e: requests.exceptions.RequestException, url: str):\n with belief_scope(\"_handle_network_error\"):\n if isinstance(e, requests.exceptions.Timeout):\n msg = \"Request timeout\"\n elif isinstance(e, requests.exceptions.ConnectionError):\n msg = \"Connection error\"\n else:\n msg = f\"Unknown network error: {e}\"\n raise NetworkError(msg, url=url) from e\n # #endregion _handle_network_error\n # #region upload_file [TYPE Function]\n # @PURPOSE: Загружает файл на сервер через multipart/form-data.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: file_info (Dict[str, Any]) - Информация о файле.\n # @PARAM: extra_data (Optional[Dict]) - Дополнительные данные.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: file_info must contain 'file_obj' and 'file_name'.\n # @POST: File is uploaded and response returned.\n # @RETURN: Ответ API в виде словаря.\n # @THROW: SupersetAPIError, NetworkError, TypeError.\n def upload_file(self, endpoint: str, file_info: dict[str, Any], extra_data: dict | None = None, timeout: int | None = None) -> dict:\n with belief_scope(\"upload_file\"):\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n _headers.pop('Content-Type', None)\n\n\n file_obj, file_name, form_field = file_info.get(\"file_obj\"), file_info.get(\"file_name\"), file_info.get(\"form_field\", \"file\")\n\n files_payload = {}\n if isinstance(file_obj, (str, Path)):\n with open(file_obj, 'rb') as f:\n files_payload = {form_field: (file_name, f.read(), 'application/x-zip-compressed')}\n elif isinstance(file_obj, io.BytesIO):\n files_payload = {form_field: (file_name, file_obj.getvalue(), 'application/x-zip-compressed')}\n else:\n raise TypeError(f\"Unsupported file_obj type: {type(file_obj)}\")\n\n return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)\n # #endregion upload_file\n # #region _perform_upload [TYPE Function]\n # @PURPOSE: (Helper) Выполняет POST запрос с файлом.\n # @PARAM: url (str) - URL.\n # @PARAM: files (Dict) - Файлы.\n # @PARAM: data (Optional[Dict]) - Данные.\n # @PARAM: headers (Dict) - Заголовки.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: url, files, and headers must be provided.\n # @POST: POST request is performed and JSON response returned.\n # @RETURN: Dict - Ответ.\n def _perform_upload(self, url: str, files: dict, data: dict | None, headers: dict, timeout: int | None) -> dict:\n with belief_scope(\"_perform_upload\"):\n try:\n response = self.session.post(url, files=files, data=data or {}, headers=headers, timeout=timeout or self.request_settings[\"timeout\"])\n response.raise_for_status()\n if response.status_code == 200:\n try:\n return response.json()\n except Exception as json_e:\n app_logger.debug(f\"[_perform_upload][Debug] Response is not valid JSON: {response.text[:200]}...\")\n raise SupersetAPIError(f\"API error during upload: Response is not valid JSON: {json_e}\") from json_e\n return response.json()\n except requests.exceptions.HTTPError as e:\n raise SupersetAPIError(f\"API error during upload: {e.response.text}\") from e\n except requests.exceptions.RequestException as e:\n raise NetworkError(f\"Network error during upload: {e}\", url=url) from e\n # #endregion _perform_upload\n # #region fetch_paginated_count [TYPE Function]\n # @PURPOSE: Получает общее количество элементов для пагинации.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: query_params (Dict) - Параметры запроса.\n # @PARAM: count_field (str) - Поле с количеством.\n # @PRE: query_params must be a dictionary.\n # @POST: Returns total count of items.\n # @RETURN: int - Количество.\n def fetch_paginated_count(self, endpoint: str, query_params: dict, count_field: str = \"count\") -> int:\n with belief_scope(\"fetch_paginated_count\"):\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query_params)}))\n return response_json.get(count_field, 0)\n # #endregion fetch_paginated_count\n # #region fetch_paginated_data [TYPE Function]\n # @PURPOSE: Автоматически собирает данные со всех страниц пагинированного эндпоинта.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: pagination_options (Dict[str, Any]) - Опции пагинации.\n # @PRE: pagination_options must contain 'base_query', 'results_field'. 'total_count' is optional.\n # @POST: Returns all items across all pages.\n # @RETURN: List[Any] - Список данных.\n def fetch_paginated_data(self, endpoint: str, pagination_options: dict[str, Any]) -> list[Any]:\n with belief_scope(\"fetch_paginated_data\"):\n base_query = pagination_options[\"base_query\"]\n total_count = pagination_options.get(\"total_count\")\n\n results_field = pagination_options[\"results_field\"]\n count_field = pagination_options.get(\"count_field\", \"count\")\n page_size = base_query.get('page_size', 1000)\n assert page_size > 0, \"'page_size' must be a positive number.\"\n\n results = []\n page = 0\n\n # Fetch first page to get data and total count if not provided\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n\n first_page_results = response_json.get(results_field, [])\n results.extend(first_page_results)\n\n if total_count is None:\n total_count = response_json.get(count_field, len(first_page_results))\n app_logger.debug(f\"[fetch_paginated_data][State] Total count resolved from first page: {total_count}\")\n # Fetch remaining pages\n total_pages = (total_count + page_size - 1) // page_size\n for page in range(1, total_pages):\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n results.extend(response_json.get(results_field, []))\n\n return results\n # #endregion fetch_paginated_data\n# #endregion APIClient\n# #endregion NetworkModule\n" + "body": "# #region NetworkModule [C:3] [TYPE Module] [SEMANTICS network, http, retry, tenacity, session]\n#\n# @BRIEF Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PUBLIC_API: APIClient\nimport io\nimport json\nimport threading\nimport time\nfrom pathlib import Path\nfrom typing import Any, cast\n\nimport requests\nimport urllib3\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\n\n# #region SupersetAPIError [C:1] [TYPE Class]\n# @BRIEF Base exception for all Superset API related errors.\nclass SupersetAPIError(Exception):\n # #region __init__ [TYPE Function] [C:1]\n # @PURPOSE: Initializes the exception with a message and context.\n # @PRE: message is a string, context is a dict.\n # @POST: Exception is initialized with context.\n def __init__(self, message: str = \"Superset API error\", **context: Any):\n with belief_scope(\"SupersetAPIError.__init__\"):\n self.context = context\n super().__init__(f\"[API_FAILURE] {message} | Context: {self.context}\")\n # #endregion __init__\n# #endregion SupersetAPIError\n# #region AuthenticationError [C:1] [TYPE Class]\n# @BRIEF Exception raised when authentication fails.\nclass AuthenticationError(SupersetAPIError):\n # #region __init__ [TYPE Function] [C:1]\n # @PURPOSE: Initializes the authentication error.\n # @PRE: message is a string, context is a dict.\n # @POST: AuthenticationError is initialized.\n def __init__(self, message: str = \"Authentication failed\", **context: Any):\n with belief_scope(\"AuthenticationError.__init__\"):\n super().__init__(message, type=\"authentication\", **context)\n # #endregion __init__\n# #endregion AuthenticationError\n# #region PermissionDeniedError [TYPE Class]\n# @BRIEF Exception raised when access is denied.\nclass PermissionDeniedError(AuthenticationError):\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the permission denied error.\n # @PRE: message is a string, context is a dict.\n # @POST: PermissionDeniedError is initialized.\n def __init__(self, message: str = \"Permission denied\", **context: Any):\n with belief_scope(\"PermissionDeniedError.__init__\"):\n super().__init__(message, **context)\n # #endregion __init__\n# #endregion PermissionDeniedError\n# #region DashboardNotFoundError [TYPE Class]\n# @BRIEF Exception raised when a dashboard cannot be found.\nclass DashboardNotFoundError(SupersetAPIError):\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the not found error with resource ID.\n # @PRE: resource_id is provided.\n # @POST: DashboardNotFoundError is initialized.\n def __init__(self, resource_id: int | str, message: str = \"Dashboard not found\", **context: Any):\n with belief_scope(\"DashboardNotFoundError.__init__\"):\n super().__init__(f\"Dashboard '{resource_id}' {message}\", subtype=\"not_found\", resource_id=resource_id, **context)\n # #endregion __init__\n# #endregion DashboardNotFoundError\n# #region NetworkError [TYPE Class]\n# @BRIEF Exception raised when a network level error occurs.\nclass NetworkError(Exception):\n # #region NetworkError.__init__ [TYPE Function]\n # @PURPOSE: Initializes the network error.\n # @PRE: message is a string.\n # @POST: NetworkError is initialized.\n def __init__(self, message: str = \"Network connection failed\", **context: Any):\n with belief_scope(\"NetworkError.__init__\"):\n self.context = context\n super().__init__(f\"[NETWORK_FAILURE] {message} | Context: {self.context}\")\n # #endregion NetworkError.__init__\n# #endregion NetworkError\n# #region SupersetAuthCache [TYPE Class]\n# @BRIEF Process-local cache for Superset access/csrf tokens keyed by environment credentials.\n# @PRE: base_url and username are stable strings.\n# @POST: Cached entries expire automatically by TTL and can be reused across requests.\nclass SupersetAuthCache:\n TTL_SECONDS = 300\n _lock = threading.Lock()\n _entries: dict[tuple[str, str, bool], dict[str, Any]] = {}\n @classmethod\n def build_key(cls, base_url: str, auth: dict[str, Any] | None, verify_ssl: bool) -> tuple[str, str, bool]:\n username = \"\"\n if isinstance(auth, dict):\n username = str(auth.get(\"username\") or \"\").strip()\n return (str(base_url or \"\").strip(), username, bool(verify_ssl))\n @classmethod\n # #region SupersetAuthCache.get [TYPE Function]\n def get(cls, key: tuple[str, str, bool]) -> dict[str, str] | None:\n now = time.time()\n with cls._lock:\n payload = cls._entries.get(key)\n if not payload:\n return None\n expires_at = float(payload.get(\"expires_at\") or 0)\n if expires_at <= now:\n cls._entries.pop(key, None)\n return None\n tokens = payload.get(\"tokens\")\n if not isinstance(tokens, dict):\n cls._entries.pop(key, None)\n return None\n return {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n }\n # #endregion SupersetAuthCache.get\n @classmethod\n # #region SupersetAuthCache.set [TYPE Function]\n def set(cls, key: tuple[str, str, bool], tokens: dict[str, str], ttl_seconds: int | None = None) -> None:\n normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)\n with cls._lock:\n cls._entries[key] = {\n \"tokens\": {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n },\n \"expires_at\": time.time() + normalized_ttl,\n }\n # #endregion SupersetAuthCache.set\n @classmethod\n def invalidate(cls, key: tuple[str, str, bool]) -> None:\n with cls._lock:\n cls._entries.pop(key, None)\n# #endregion SupersetAuthCache\n# #region APIClient [C:3] [TYPE Class]\n# @BRIEF Synchronous Superset API client with process-local auth token caching.\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @RELATION DEPENDS_ON -> [LoggerModule]\nclass APIClient:\n DEFAULT_TIMEOUT = 30\n # #region APIClient.__init__ [TYPE Function]\n # @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером.\n # @PARAM: config (Dict[str, Any]) - Конфигурация.\n # @PARAM: verify_ssl (bool) - Проверять ли SSL.\n # @PARAM: timeout (int) - Таймаут запросов.\n # @PRE: config must contain 'base_url' and 'auth'.\n # @POST: APIClient instance is initialized with a session.\n def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\n with belief_scope(\"__init__\"):\n app_logger.reason(\"Initializing APIClient.\", extra={\"src\": \"APIClient.__init__\"})\n self.base_url: str = self._normalize_base_url(config.get(\"base_url\", \"\"))\n self.api_base_url: str = f\"{self.base_url}/api/v1\"\n self.auth = config.get(\"auth\")\n self.request_settings = {\"verify_ssl\": verify_ssl, \"timeout\": timeout}\n self.session = self._init_session()\n self._tokens: dict[str, str] = {}\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n self._authenticated = False\n app_logger.reflect(\"APIClient initialized.\", extra={\"src\": \"APIClient.__init__\"})\n # #endregion APIClient.__init__\n # #region _init_session [TYPE Function]\n # @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой.\n # @PRE: self.request_settings must be initialized.\n # @POST: Returns a configured requests.Session instance.\n # @RETURN: requests.Session - Настроенная сессия.\n def _init_session(self) -> requests.Session:\n with belief_scope(\"_init_session\"):\n session = requests.Session()\n\n # Create a custom adapter that handles TLS issues\n class TLSAdapter(HTTPAdapter):\n def init_poolmanager(self, connections, maxsize, block=False):\n import ssl\n\n from urllib3.poolmanager import PoolManager\n\n # Create an SSL context that ignores TLSv1 unrecognized name errors\n ctx = ssl.create_default_context()\n ctx.set_ciphers('HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')\n\n # Ignore TLSV1_UNRECOGNIZED_NAME errors by disabling hostname verification\n # This is safe when verify_ssl is false (we're already not verifying the certificate)\n ctx.check_hostname = False\n\n self.poolmanager = PoolManager(\n num_pools=connections,\n maxsize=maxsize,\n block=block,\n ssl_context=ctx\n )\n\n retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])\n adapter = TLSAdapter(max_retries=retries)\n session.mount('http://', adapter)\n session.mount('https://', adapter)\n\n if not self.request_settings[\"verify_ssl\"]:\n urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n app_logger.warning(\"[_init_session][State] SSL verification disabled.\")\n # When verify_ssl is false, we should also disable hostname verification\n session.verify = False\n else:\n session.verify = True\n\n return session\n # #endregion _init_session\n # #region _normalize_base_url [TYPE Function]\n # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.\n # Auto-prepends https:// if no scheme is present.\n # @PRE: raw_url can be empty.\n # @POST: Returns canonical base URL with scheme, suitable for building API endpoints.\n # @RETURN: str\n def _normalize_base_url(self, raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n normalized = normalized.rstrip(\"/\")\n # Auto-prepend https:// if no scheme is present\n if normalized and not normalized.startswith(\"http://\") and not normalized.startswith(\"https://\"):\n normalized = f\"https://{normalized}\"\n return normalized\n # #endregion _normalize_base_url\n # #region _build_api_url [TYPE Function]\n # @PURPOSE: Build absolute Superset API URL for endpoint using canonical /api/v1 base.\n # @PRE: endpoint is relative path or absolute URL.\n # @POST: Returns full URL without accidental duplicate slashes.\n # @RETURN: str\n def _build_api_url(self, endpoint: str) -> str:\n normalized_endpoint = str(endpoint or \"\").strip()\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n return normalized_endpoint\n if not normalized_endpoint.startswith(\"/\"):\n normalized_endpoint = f\"/{normalized_endpoint}\"\n if normalized_endpoint.startswith(\"/api/v1/\") or normalized_endpoint == \"/api/v1\":\n return f\"{self.base_url}{normalized_endpoint}\"\n return f\"{self.api_base_url}{normalized_endpoint}\"\n # #endregion _build_api_url\n # #region APIClient.authenticate [TYPE Function]\n # @PURPOSE: Выполняет аутентификацию в Superset API и получает access и CSRF токены.\n # @PRE: self.auth and self.base_url must be valid.\n # @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`.\n # @RETURN: Dict[str, str] - Словарь с токенами.\n # @THROW: AuthenticationError, NetworkError - при ошибках.\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n def authenticate(self) -> dict[str, str]:\n with belief_scope(\"authenticate\"):\n app_logger.info(\"[authenticate][Enter] Authenticating to %s\", self.base_url)\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\"):\n # Cache hit — we have a valid access_token, but we need to\n # re-establish the session cookie in this new requests.Session.\n # Superset CSRF protection pairs the X-CSRFToken header with a\n # Flask session cookie set during login. A fresh requests.Session\n # lacks that cookie, so even a valid cached CSRF token would fail\n # with \"CSRF session token is missing\". We re-fetch the CSRF\n # token to both establish the session cookie AND get a fresh\n # CSRF token for the current session.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {cached_tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n csrf_token = csrf_response.json()[\"result\"]\n self._tokens = {\n \"access_token\": cached_tokens[\"access_token\"],\n \"csrf_token\": csrf_token,\n }\n self._authenticated = True\n # Update cache with fresh CSRF token for this session\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.info(\n \"[authenticate][CacheHit] Reusing cached Superset auth tokens for %s \"\n \"(CSRF token refreshed for new session)\",\n self.base_url,\n )\n return self._tokens\n except Exception as refresh_err:\n app_logger.warning(\n \"[authenticate][CacheRefreshFailed] CSRF token refresh failed, \"\n \"falling back to full re-authentication: %s\",\n refresh_err,\n )\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full authentication below\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n # Log the payload keys and values (masking password)\n masked_auth = {k: (\"******\" if k == \"password\" else v) for k, v in self.auth.items()}\n app_logger.info(f\"[authenticate][Debug] Login URL: {login_url}\")\n app_logger.info(f\"[authenticate][Debug] Auth payload: {masked_auth}\")\n\n response = self.session.post(login_url, json=self.auth, timeout=self.request_settings[\"timeout\"])\n\n if response.status_code != 200:\n app_logger.error(f\"[authenticate][Error] Status: {response.status_code}, Response: {response.text}\")\n\n response.raise_for_status()\n access_token = response.json()[\"access_token\"]\n\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(csrf_url, headers={\"Authorization\": f\"Bearer {access_token}\"}, timeout=self.request_settings[\"timeout\"])\n csrf_response.raise_for_status()\n\n self._tokens = {\"access_token\": access_token, \"csrf_token\": csrf_response.json()[\"result\"]}\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.reflect(\"Authenticated successfully.\", extra={\"src\": \"authenticate\"})\n return self._tokens\n except requests.exceptions.HTTPError as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = e.response.status_code if e.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable during authentication (Status {status_code})\", status_code=status_code) from e\n raise AuthenticationError(f\"Authentication failed: {e}\") from e\n except (requests.exceptions.RequestException, KeyError) as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {e}\") from e\n # #endregion APIClient.authenticate\n @property\n # #region headers [TYPE Function]\n # @PURPOSE: Возвращает HTTP-заголовки для аутентифицированных запросов.\n # @PRE: APIClient is initialized and authenticated or can be authenticated.\n # @POST: Returns headers including auth tokens.\n def headers(self) -> dict[str, str]:\n if not self._authenticated:\n self.authenticate()\n return {\n \"Authorization\": f\"Bearer {self._tokens['access_token']}\",\n \"X-CSRFToken\": self._tokens.get(\"csrf_token\", \"\"),\n \"Referer\": self.base_url,\n \"Content-Type\": \"application/json\"\n }\n # #endregion headers\n # #region request [TYPE Function]\n # @PURPOSE: Выполняет универсальный HTTP-запрос к API.\n # @PARAM: method (str) - HTTP метод.\n # @PARAM: endpoint (str) - API эндпоинт.\n # @PARAM: headers (Optional[Dict]) - Дополнительные заголовки.\n # @PARAM: raw_response (bool) - Возвращать ли сырой ответ.\n # @PRE: method and endpoint must be strings.\n # @POST: Returns response content or raw Response object.\n # @RETURN: `requests.Response` если `raw_response=True`, иначе `dict`.\n # @THROW: SupersetAPIError, NetworkError и их подклассы.\n def request(self, method: str, endpoint: str, headers: dict | None = None, raw_response: bool = False, **kwargs) -> requests.Response | dict[str, Any]:\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n if headers:\n _headers.update(headers)\n\n try:\n response = self.session.request(method, full_url, headers=_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except requests.exceptions.HTTPError as e:\n if e.response is not None and e.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(e, endpoint)\n except requests.exceptions.RequestException as e:\n self._handle_network_error(e, full_url)\n # #endregion request\n # #region _handle_http_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует HTTP ошибки в кастомные исключения.\n # @PARAM: e (requests.exceptions.HTTPError) - Ошибка.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PRE: e must be a valid HTTPError with a response.\n # @POST: Raises a specific SupersetAPIError or subclass.\n def _handle_http_error(self, e: requests.exceptions.HTTPError, endpoint: str):\n with belief_scope(\"_handle_http_error\"):\n status_code = e.response.status_code\n if status_code == 502 or status_code == 503 or status_code == 504:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from e\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from e\n raise SupersetAPIError(\n f\"API resource not found at endpoint '{endpoint}'\",\n status_code=status_code,\n endpoint=endpoint,\n subtype=\"not_found\",\n ) from e\n if status_code == 403:\n raise PermissionDeniedError() from e\n if status_code == 401:\n raise AuthenticationError() from e\n raise SupersetAPIError(f\"API Error {status_code}: {e.response.text}\") from e\n # #endregion _handle_http_error\n # #region _is_dashboard_endpoint [TYPE Function]\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\n # @PRE: endpoint may be relative or absolute.\n # @POST: Returns true only for dashboard-specific endpoints.\n def _is_dashboard_endpoint(self, endpoint: str) -> bool:\n normalized_endpoint = str(endpoint or \"\").strip().lower()\n if not normalized_endpoint:\n return False\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n try:\n normalized_endpoint = \"/\" + normalized_endpoint.split(\"/api/v1\", 1)[1].lstrip(\"/\")\n except IndexError:\n return False\n if normalized_endpoint.startswith(\"/api/v1/\"):\n normalized_endpoint = normalized_endpoint[len(\"/api/v1\"):]\n return normalized_endpoint.startswith(\"/dashboard/\") or normalized_endpoint == \"/dashboard\"\n # #endregion _is_dashboard_endpoint\n # #region _handle_network_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует сетевые ошибки в `NetworkError`.\n # @PARAM: e (requests.exceptions.RequestException) - Ошибка.\n # @PARAM: url (str) - URL.\n # @PRE: e must be a RequestException.\n # @POST: Raises a NetworkError.\n def _handle_network_error(self, e: requests.exceptions.RequestException, url: str):\n with belief_scope(\"_handle_network_error\"):\n if isinstance(e, requests.exceptions.Timeout):\n msg = \"Request timeout\"\n elif isinstance(e, requests.exceptions.ConnectionError):\n msg = \"Connection error\"\n else:\n msg = f\"Unknown network error: {e}\"\n raise NetworkError(msg, url=url) from e\n # #endregion _handle_network_error\n # #region upload_file [TYPE Function]\n # @PURPOSE: Загружает файл на сервер через multipart/form-data.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: file_info (Dict[str, Any]) - Информация о файле.\n # @PARAM: extra_data (Optional[Dict]) - Дополнительные данные.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: file_info must contain 'file_obj' and 'file_name'.\n # @POST: File is uploaded and response returned.\n # @RETURN: Ответ API в виде словаря.\n # @THROW: SupersetAPIError, NetworkError, TypeError.\n def upload_file(self, endpoint: str, file_info: dict[str, Any], extra_data: dict | None = None, timeout: int | None = None) -> dict:\n with belief_scope(\"upload_file\"):\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n _headers.pop('Content-Type', None)\n\n\n file_obj, file_name, form_field = file_info.get(\"file_obj\"), file_info.get(\"file_name\"), file_info.get(\"form_field\", \"file\")\n\n files_payload = {}\n if isinstance(file_obj, (str, Path)):\n with open(file_obj, 'rb') as f:\n files_payload = {form_field: (file_name, f.read(), 'application/x-zip-compressed')}\n elif isinstance(file_obj, io.BytesIO):\n files_payload = {form_field: (file_name, file_obj.getvalue(), 'application/x-zip-compressed')}\n else:\n raise TypeError(f\"Unsupported file_obj type: {type(file_obj)}\")\n\n return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)\n # #endregion upload_file\n # #region _perform_upload [TYPE Function]\n # @PURPOSE: (Helper) Выполняет POST запрос с файлом.\n # @PARAM: url (str) - URL.\n # @PARAM: files (Dict) - Файлы.\n # @PARAM: data (Optional[Dict]) - Данные.\n # @PARAM: headers (Dict) - Заголовки.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: url, files, and headers must be provided.\n # @POST: POST request is performed and JSON response returned.\n # @RETURN: Dict - Ответ.\n def _perform_upload(self, url: str, files: dict, data: dict | None, headers: dict, timeout: int | None) -> dict:\n with belief_scope(\"_perform_upload\"):\n try:\n response = self.session.post(url, files=files, data=data or {}, headers=headers, timeout=timeout or self.request_settings[\"timeout\"])\n response.raise_for_status()\n if response.status_code == 200:\n try:\n return response.json()\n except Exception as json_e:\n app_logger.debug(f\"[_perform_upload][Debug] Response is not valid JSON: {response.text[:200]}...\")\n raise SupersetAPIError(f\"API error during upload: Response is not valid JSON: {json_e}\") from json_e\n return response.json()\n except requests.exceptions.HTTPError as e:\n raise SupersetAPIError(f\"API error during upload: {e.response.text}\") from e\n except requests.exceptions.RequestException as e:\n raise NetworkError(f\"Network error during upload: {e}\", url=url) from e\n # #endregion _perform_upload\n # #region fetch_paginated_count [TYPE Function]\n # @PURPOSE: Получает общее количество элементов для пагинации.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: query_params (Dict) - Параметры запроса.\n # @PARAM: count_field (str) - Поле с количеством.\n # @PRE: query_params must be a dictionary.\n # @POST: Returns total count of items.\n # @RETURN: int - Количество.\n def fetch_paginated_count(self, endpoint: str, query_params: dict, count_field: str = \"count\") -> int:\n with belief_scope(\"fetch_paginated_count\"):\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query_params)}))\n return response_json.get(count_field, 0)\n # #endregion fetch_paginated_count\n # #region fetch_paginated_data [TYPE Function]\n # @PURPOSE: Автоматически собирает данные со всех страниц пагинированного эндпоинта.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: pagination_options (Dict[str, Any]) - Опции пагинации.\n # @PRE: pagination_options must contain 'base_query', 'results_field'. 'total_count' is optional.\n # @POST: Returns all items across all pages.\n # @RETURN: List[Any] - Список данных.\n def fetch_paginated_data(self, endpoint: str, pagination_options: dict[str, Any]) -> list[Any]:\n with belief_scope(\"fetch_paginated_data\"):\n base_query = pagination_options[\"base_query\"]\n total_count = pagination_options.get(\"total_count\")\n\n results_field = pagination_options[\"results_field\"]\n count_field = pagination_options.get(\"count_field\", \"count\")\n page_size = base_query.get('page_size', 1000)\n assert page_size > 0, \"'page_size' must be a positive number.\"\n\n results = []\n page = 0\n\n # Fetch first page to get data and total count if not provided\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n\n first_page_results = response_json.get(results_field, [])\n results.extend(first_page_results)\n\n if total_count is None:\n total_count = response_json.get(count_field, len(first_page_results))\n app_logger.debug(f\"[fetch_paginated_data][State] Total count resolved from first page: {total_count}\")\n # Fetch remaining pages\n total_pages = (total_count + page_size - 1) // page_size\n for page in range(1, total_pages):\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n results.extend(response_json.get(results_field, []))\n\n return results\n # #endregion fetch_paginated_data\n# #endregion APIClient\n# #endregion NetworkModule\n" }, { "contract_id": "SupersetAPIError", @@ -39502,7 +40345,7 @@ "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", "start_line": 138, - "end_line": 537, + "end_line": 542, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -39526,7 +40369,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region APIClient [C:3] [TYPE Class]\n# @BRIEF Synchronous Superset API client with process-local auth token caching.\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @RELATION DEPENDS_ON -> [LoggerModule]\nclass APIClient:\n DEFAULT_TIMEOUT = 30\n # #region APIClient.__init__ [TYPE Function]\n # @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером.\n # @PARAM: config (Dict[str, Any]) - Конфигурация.\n # @PARAM: verify_ssl (bool) - Проверять ли SSL.\n # @PARAM: timeout (int) - Таймаут запросов.\n # @PRE: config must contain 'base_url' and 'auth'.\n # @POST: APIClient instance is initialized with a session.\n def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\n with belief_scope(\"__init__\"):\n app_logger.reason(\"Initializing APIClient.\", extra={\"src\": \"APIClient.__init__\"})\n self.base_url: str = self._normalize_base_url(config.get(\"base_url\", \"\"))\n self.api_base_url: str = f\"{self.base_url}/api/v1\"\n self.auth = config.get(\"auth\")\n self.request_settings = {\"verify_ssl\": verify_ssl, \"timeout\": timeout}\n self.session = self._init_session()\n self._tokens: dict[str, str] = {}\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n self._authenticated = False\n app_logger.reflect(\"APIClient initialized.\", extra={\"src\": \"APIClient.__init__\"})\n # #endregion APIClient.__init__\n # #region _init_session [TYPE Function]\n # @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой.\n # @PRE: self.request_settings must be initialized.\n # @POST: Returns a configured requests.Session instance.\n # @RETURN: requests.Session - Настроенная сессия.\n def _init_session(self) -> requests.Session:\n with belief_scope(\"_init_session\"):\n session = requests.Session()\n\n # Create a custom adapter that handles TLS issues\n class TLSAdapter(HTTPAdapter):\n def init_poolmanager(self, connections, maxsize, block=False):\n import ssl\n\n from urllib3.poolmanager import PoolManager\n\n # Create an SSL context that ignores TLSv1 unrecognized name errors\n ctx = ssl.create_default_context()\n ctx.set_ciphers('HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')\n\n # Ignore TLSV1_UNRECOGNIZED_NAME errors by disabling hostname verification\n # This is safe when verify_ssl is false (we're already not verifying the certificate)\n ctx.check_hostname = False\n\n self.poolmanager = PoolManager(\n num_pools=connections,\n maxsize=maxsize,\n block=block,\n ssl_context=ctx\n )\n\n retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])\n adapter = TLSAdapter(max_retries=retries)\n session.mount('http://', adapter)\n session.mount('https://', adapter)\n\n if not self.request_settings[\"verify_ssl\"]:\n urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n app_logger.warning(\"[_init_session][State] SSL verification disabled.\")\n # When verify_ssl is false, we should also disable hostname verification\n session.verify = False\n else:\n session.verify = True\n\n return session\n # #endregion _init_session\n # #region _normalize_base_url [TYPE Function]\n # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.\n # @PRE: raw_url can be empty.\n # @POST: Returns canonical base URL suitable for building API endpoints.\n # @RETURN: str\n def _normalize_base_url(self, raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n return normalized.rstrip(\"/\")\n # #endregion _normalize_base_url\n # #region _build_api_url [TYPE Function]\n # @PURPOSE: Build absolute Superset API URL for endpoint using canonical /api/v1 base.\n # @PRE: endpoint is relative path or absolute URL.\n # @POST: Returns full URL without accidental duplicate slashes.\n # @RETURN: str\n def _build_api_url(self, endpoint: str) -> str:\n normalized_endpoint = str(endpoint or \"\").strip()\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n return normalized_endpoint\n if not normalized_endpoint.startswith(\"/\"):\n normalized_endpoint = f\"/{normalized_endpoint}\"\n if normalized_endpoint.startswith(\"/api/v1/\") or normalized_endpoint == \"/api/v1\":\n return f\"{self.base_url}{normalized_endpoint}\"\n return f\"{self.api_base_url}{normalized_endpoint}\"\n # #endregion _build_api_url\n # #region APIClient.authenticate [TYPE Function]\n # @PURPOSE: Выполняет аутентификацию в Superset API и получает access и CSRF токены.\n # @PRE: self.auth and self.base_url must be valid.\n # @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`.\n # @RETURN: Dict[str, str] - Словарь с токенами.\n # @THROW: AuthenticationError, NetworkError - при ошибках.\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n def authenticate(self) -> dict[str, str]:\n with belief_scope(\"authenticate\"):\n app_logger.info(\"[authenticate][Enter] Authenticating to %s\", self.base_url)\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\"):\n # Cache hit — we have a valid access_token, but we need to\n # re-establish the session cookie in this new requests.Session.\n # Superset CSRF protection pairs the X-CSRFToken header with a\n # Flask session cookie set during login. A fresh requests.Session\n # lacks that cookie, so even a valid cached CSRF token would fail\n # with \"CSRF session token is missing\". We re-fetch the CSRF\n # token to both establish the session cookie AND get a fresh\n # CSRF token for the current session.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {cached_tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n csrf_token = csrf_response.json()[\"result\"]\n self._tokens = {\n \"access_token\": cached_tokens[\"access_token\"],\n \"csrf_token\": csrf_token,\n }\n self._authenticated = True\n # Update cache with fresh CSRF token for this session\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.info(\n \"[authenticate][CacheHit] Reusing cached Superset auth tokens for %s \"\n \"(CSRF token refreshed for new session)\",\n self.base_url,\n )\n return self._tokens\n except Exception as refresh_err:\n app_logger.warning(\n \"[authenticate][CacheRefreshFailed] CSRF token refresh failed, \"\n \"falling back to full re-authentication: %s\",\n refresh_err,\n )\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full authentication below\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n # Log the payload keys and values (masking password)\n masked_auth = {k: (\"******\" if k == \"password\" else v) for k, v in self.auth.items()}\n app_logger.info(f\"[authenticate][Debug] Login URL: {login_url}\")\n app_logger.info(f\"[authenticate][Debug] Auth payload: {masked_auth}\")\n\n response = self.session.post(login_url, json=self.auth, timeout=self.request_settings[\"timeout\"])\n\n if response.status_code != 200:\n app_logger.error(f\"[authenticate][Error] Status: {response.status_code}, Response: {response.text}\")\n\n response.raise_for_status()\n access_token = response.json()[\"access_token\"]\n\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(csrf_url, headers={\"Authorization\": f\"Bearer {access_token}\"}, timeout=self.request_settings[\"timeout\"])\n csrf_response.raise_for_status()\n\n self._tokens = {\"access_token\": access_token, \"csrf_token\": csrf_response.json()[\"result\"]}\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.reflect(\"Authenticated successfully.\", extra={\"src\": \"authenticate\"})\n return self._tokens\n except requests.exceptions.HTTPError as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = e.response.status_code if e.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable during authentication (Status {status_code})\", status_code=status_code) from e\n raise AuthenticationError(f\"Authentication failed: {e}\") from e\n except (requests.exceptions.RequestException, KeyError) as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {e}\") from e\n # #endregion APIClient.authenticate\n @property\n # #region headers [TYPE Function]\n # @PURPOSE: Возвращает HTTP-заголовки для аутентифицированных запросов.\n # @PRE: APIClient is initialized and authenticated or can be authenticated.\n # @POST: Returns headers including auth tokens.\n def headers(self) -> dict[str, str]:\n if not self._authenticated:\n self.authenticate()\n return {\n \"Authorization\": f\"Bearer {self._tokens['access_token']}\",\n \"X-CSRFToken\": self._tokens.get(\"csrf_token\", \"\"),\n \"Referer\": self.base_url,\n \"Content-Type\": \"application/json\"\n }\n # #endregion headers\n # #region request [TYPE Function]\n # @PURPOSE: Выполняет универсальный HTTP-запрос к API.\n # @PARAM: method (str) - HTTP метод.\n # @PARAM: endpoint (str) - API эндпоинт.\n # @PARAM: headers (Optional[Dict]) - Дополнительные заголовки.\n # @PARAM: raw_response (bool) - Возвращать ли сырой ответ.\n # @PRE: method and endpoint must be strings.\n # @POST: Returns response content or raw Response object.\n # @RETURN: `requests.Response` если `raw_response=True`, иначе `dict`.\n # @THROW: SupersetAPIError, NetworkError и их подклассы.\n def request(self, method: str, endpoint: str, headers: dict | None = None, raw_response: bool = False, **kwargs) -> requests.Response | dict[str, Any]:\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n if headers:\n _headers.update(headers)\n\n try:\n response = self.session.request(method, full_url, headers=_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except requests.exceptions.HTTPError as e:\n if e.response is not None and e.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(e, endpoint)\n except requests.exceptions.RequestException as e:\n self._handle_network_error(e, full_url)\n # #endregion request\n # #region _handle_http_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует HTTP ошибки в кастомные исключения.\n # @PARAM: e (requests.exceptions.HTTPError) - Ошибка.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PRE: e must be a valid HTTPError with a response.\n # @POST: Raises a specific SupersetAPIError or subclass.\n def _handle_http_error(self, e: requests.exceptions.HTTPError, endpoint: str):\n with belief_scope(\"_handle_http_error\"):\n status_code = e.response.status_code\n if status_code == 502 or status_code == 503 or status_code == 504:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from e\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from e\n raise SupersetAPIError(\n f\"API resource not found at endpoint '{endpoint}'\",\n status_code=status_code,\n endpoint=endpoint,\n subtype=\"not_found\",\n ) from e\n if status_code == 403:\n raise PermissionDeniedError() from e\n if status_code == 401:\n raise AuthenticationError() from e\n raise SupersetAPIError(f\"API Error {status_code}: {e.response.text}\") from e\n # #endregion _handle_http_error\n # #region _is_dashboard_endpoint [TYPE Function]\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\n # @PRE: endpoint may be relative or absolute.\n # @POST: Returns true only for dashboard-specific endpoints.\n def _is_dashboard_endpoint(self, endpoint: str) -> bool:\n normalized_endpoint = str(endpoint or \"\").strip().lower()\n if not normalized_endpoint:\n return False\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n try:\n normalized_endpoint = \"/\" + normalized_endpoint.split(\"/api/v1\", 1)[1].lstrip(\"/\")\n except IndexError:\n return False\n if normalized_endpoint.startswith(\"/api/v1/\"):\n normalized_endpoint = normalized_endpoint[len(\"/api/v1\"):]\n return normalized_endpoint.startswith(\"/dashboard/\") or normalized_endpoint == \"/dashboard\"\n # #endregion _is_dashboard_endpoint\n # #region _handle_network_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует сетевые ошибки в `NetworkError`.\n # @PARAM: e (requests.exceptions.RequestException) - Ошибка.\n # @PARAM: url (str) - URL.\n # @PRE: e must be a RequestException.\n # @POST: Raises a NetworkError.\n def _handle_network_error(self, e: requests.exceptions.RequestException, url: str):\n with belief_scope(\"_handle_network_error\"):\n if isinstance(e, requests.exceptions.Timeout):\n msg = \"Request timeout\"\n elif isinstance(e, requests.exceptions.ConnectionError):\n msg = \"Connection error\"\n else:\n msg = f\"Unknown network error: {e}\"\n raise NetworkError(msg, url=url) from e\n # #endregion _handle_network_error\n # #region upload_file [TYPE Function]\n # @PURPOSE: Загружает файл на сервер через multipart/form-data.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: file_info (Dict[str, Any]) - Информация о файле.\n # @PARAM: extra_data (Optional[Dict]) - Дополнительные данные.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: file_info must contain 'file_obj' and 'file_name'.\n # @POST: File is uploaded and response returned.\n # @RETURN: Ответ API в виде словаря.\n # @THROW: SupersetAPIError, NetworkError, TypeError.\n def upload_file(self, endpoint: str, file_info: dict[str, Any], extra_data: dict | None = None, timeout: int | None = None) -> dict:\n with belief_scope(\"upload_file\"):\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n _headers.pop('Content-Type', None)\n\n\n file_obj, file_name, form_field = file_info.get(\"file_obj\"), file_info.get(\"file_name\"), file_info.get(\"form_field\", \"file\")\n\n files_payload = {}\n if isinstance(file_obj, (str, Path)):\n with open(file_obj, 'rb') as f:\n files_payload = {form_field: (file_name, f.read(), 'application/x-zip-compressed')}\n elif isinstance(file_obj, io.BytesIO):\n files_payload = {form_field: (file_name, file_obj.getvalue(), 'application/x-zip-compressed')}\n else:\n raise TypeError(f\"Unsupported file_obj type: {type(file_obj)}\")\n\n return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)\n # #endregion upload_file\n # #region _perform_upload [TYPE Function]\n # @PURPOSE: (Helper) Выполняет POST запрос с файлом.\n # @PARAM: url (str) - URL.\n # @PARAM: files (Dict) - Файлы.\n # @PARAM: data (Optional[Dict]) - Данные.\n # @PARAM: headers (Dict) - Заголовки.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: url, files, and headers must be provided.\n # @POST: POST request is performed and JSON response returned.\n # @RETURN: Dict - Ответ.\n def _perform_upload(self, url: str, files: dict, data: dict | None, headers: dict, timeout: int | None) -> dict:\n with belief_scope(\"_perform_upload\"):\n try:\n response = self.session.post(url, files=files, data=data or {}, headers=headers, timeout=timeout or self.request_settings[\"timeout\"])\n response.raise_for_status()\n if response.status_code == 200:\n try:\n return response.json()\n except Exception as json_e:\n app_logger.debug(f\"[_perform_upload][Debug] Response is not valid JSON: {response.text[:200]}...\")\n raise SupersetAPIError(f\"API error during upload: Response is not valid JSON: {json_e}\") from json_e\n return response.json()\n except requests.exceptions.HTTPError as e:\n raise SupersetAPIError(f\"API error during upload: {e.response.text}\") from e\n except requests.exceptions.RequestException as e:\n raise NetworkError(f\"Network error during upload: {e}\", url=url) from e\n # #endregion _perform_upload\n # #region fetch_paginated_count [TYPE Function]\n # @PURPOSE: Получает общее количество элементов для пагинации.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: query_params (Dict) - Параметры запроса.\n # @PARAM: count_field (str) - Поле с количеством.\n # @PRE: query_params must be a dictionary.\n # @POST: Returns total count of items.\n # @RETURN: int - Количество.\n def fetch_paginated_count(self, endpoint: str, query_params: dict, count_field: str = \"count\") -> int:\n with belief_scope(\"fetch_paginated_count\"):\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query_params)}))\n return response_json.get(count_field, 0)\n # #endregion fetch_paginated_count\n # #region fetch_paginated_data [TYPE Function]\n # @PURPOSE: Автоматически собирает данные со всех страниц пагинированного эндпоинта.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: pagination_options (Dict[str, Any]) - Опции пагинации.\n # @PRE: pagination_options must contain 'base_query', 'results_field'. 'total_count' is optional.\n # @POST: Returns all items across all pages.\n # @RETURN: List[Any] - Список данных.\n def fetch_paginated_data(self, endpoint: str, pagination_options: dict[str, Any]) -> list[Any]:\n with belief_scope(\"fetch_paginated_data\"):\n base_query = pagination_options[\"base_query\"]\n total_count = pagination_options.get(\"total_count\")\n\n results_field = pagination_options[\"results_field\"]\n count_field = pagination_options.get(\"count_field\", \"count\")\n page_size = base_query.get('page_size', 1000)\n assert page_size > 0, \"'page_size' must be a positive number.\"\n\n results = []\n page = 0\n\n # Fetch first page to get data and total count if not provided\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n\n first_page_results = response_json.get(results_field, [])\n results.extend(first_page_results)\n\n if total_count is None:\n total_count = response_json.get(count_field, len(first_page_results))\n app_logger.debug(f\"[fetch_paginated_data][State] Total count resolved from first page: {total_count}\")\n # Fetch remaining pages\n total_pages = (total_count + page_size - 1) // page_size\n for page in range(1, total_pages):\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n results.extend(response_json.get(results_field, []))\n\n return results\n # #endregion fetch_paginated_data\n# #endregion APIClient\n" + "body": "# #region APIClient [C:3] [TYPE Class]\n# @BRIEF Synchronous Superset API client with process-local auth token caching.\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @RELATION DEPENDS_ON -> [LoggerModule]\nclass APIClient:\n DEFAULT_TIMEOUT = 30\n # #region APIClient.__init__ [TYPE Function]\n # @PURPOSE: Инициализирует API клиент с конфигурацией, сессией и логгером.\n # @PARAM: config (Dict[str, Any]) - Конфигурация.\n # @PARAM: verify_ssl (bool) - Проверять ли SSL.\n # @PARAM: timeout (int) - Таймаут запросов.\n # @PRE: config must contain 'base_url' and 'auth'.\n # @POST: APIClient instance is initialized with a session.\n def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\n with belief_scope(\"__init__\"):\n app_logger.reason(\"Initializing APIClient.\", extra={\"src\": \"APIClient.__init__\"})\n self.base_url: str = self._normalize_base_url(config.get(\"base_url\", \"\"))\n self.api_base_url: str = f\"{self.base_url}/api/v1\"\n self.auth = config.get(\"auth\")\n self.request_settings = {\"verify_ssl\": verify_ssl, \"timeout\": timeout}\n self.session = self._init_session()\n self._tokens: dict[str, str] = {}\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n self._authenticated = False\n app_logger.reflect(\"APIClient initialized.\", extra={\"src\": \"APIClient.__init__\"})\n # #endregion APIClient.__init__\n # #region _init_session [TYPE Function]\n # @PURPOSE: Создает и настраивает `requests.Session` с retry-логикой.\n # @PRE: self.request_settings must be initialized.\n # @POST: Returns a configured requests.Session instance.\n # @RETURN: requests.Session - Настроенная сессия.\n def _init_session(self) -> requests.Session:\n with belief_scope(\"_init_session\"):\n session = requests.Session()\n\n # Create a custom adapter that handles TLS issues\n class TLSAdapter(HTTPAdapter):\n def init_poolmanager(self, connections, maxsize, block=False):\n import ssl\n\n from urllib3.poolmanager import PoolManager\n\n # Create an SSL context that ignores TLSv1 unrecognized name errors\n ctx = ssl.create_default_context()\n ctx.set_ciphers('HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')\n\n # Ignore TLSV1_UNRECOGNIZED_NAME errors by disabling hostname verification\n # This is safe when verify_ssl is false (we're already not verifying the certificate)\n ctx.check_hostname = False\n\n self.poolmanager = PoolManager(\n num_pools=connections,\n maxsize=maxsize,\n block=block,\n ssl_context=ctx\n )\n\n retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504])\n adapter = TLSAdapter(max_retries=retries)\n session.mount('http://', adapter)\n session.mount('https://', adapter)\n\n if not self.request_settings[\"verify_ssl\"]:\n urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n app_logger.warning(\"[_init_session][State] SSL verification disabled.\")\n # When verify_ssl is false, we should also disable hostname verification\n session.verify = False\n else:\n session.verify = True\n\n return session\n # #endregion _init_session\n # #region _normalize_base_url [TYPE Function]\n # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.\n # Auto-prepends https:// if no scheme is present.\n # @PRE: raw_url can be empty.\n # @POST: Returns canonical base URL with scheme, suitable for building API endpoints.\n # @RETURN: str\n def _normalize_base_url(self, raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n normalized = normalized.rstrip(\"/\")\n # Auto-prepend https:// if no scheme is present\n if normalized and not normalized.startswith(\"http://\") and not normalized.startswith(\"https://\"):\n normalized = f\"https://{normalized}\"\n return normalized\n # #endregion _normalize_base_url\n # #region _build_api_url [TYPE Function]\n # @PURPOSE: Build absolute Superset API URL for endpoint using canonical /api/v1 base.\n # @PRE: endpoint is relative path or absolute URL.\n # @POST: Returns full URL without accidental duplicate slashes.\n # @RETURN: str\n def _build_api_url(self, endpoint: str) -> str:\n normalized_endpoint = str(endpoint or \"\").strip()\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n return normalized_endpoint\n if not normalized_endpoint.startswith(\"/\"):\n normalized_endpoint = f\"/{normalized_endpoint}\"\n if normalized_endpoint.startswith(\"/api/v1/\") or normalized_endpoint == \"/api/v1\":\n return f\"{self.base_url}{normalized_endpoint}\"\n return f\"{self.api_base_url}{normalized_endpoint}\"\n # #endregion _build_api_url\n # #region APIClient.authenticate [TYPE Function]\n # @PURPOSE: Выполняет аутентификацию в Superset API и получает access и CSRF токены.\n # @PRE: self.auth and self.base_url must be valid.\n # @POST: `self._tokens` заполнен, `self._authenticated` установлен в `True`.\n # @RETURN: Dict[str, str] - Словарь с токенами.\n # @THROW: AuthenticationError, NetworkError - при ошибках.\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n def authenticate(self) -> dict[str, str]:\n with belief_scope(\"authenticate\"):\n app_logger.info(\"[authenticate][Enter] Authenticating to %s\", self.base_url)\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\"):\n # Cache hit — we have a valid access_token, but we need to\n # re-establish the session cookie in this new requests.Session.\n # Superset CSRF protection pairs the X-CSRFToken header with a\n # Flask session cookie set during login. A fresh requests.Session\n # lacks that cookie, so even a valid cached CSRF token would fail\n # with \"CSRF session token is missing\". We re-fetch the CSRF\n # token to both establish the session cookie AND get a fresh\n # CSRF token for the current session.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {cached_tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n csrf_token = csrf_response.json()[\"result\"]\n self._tokens = {\n \"access_token\": cached_tokens[\"access_token\"],\n \"csrf_token\": csrf_token,\n }\n self._authenticated = True\n # Update cache with fresh CSRF token for this session\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.info(\n \"[authenticate][CacheHit] Reusing cached Superset auth tokens for %s \"\n \"(CSRF token refreshed for new session)\",\n self.base_url,\n )\n return self._tokens\n except Exception as refresh_err:\n app_logger.warning(\n \"[authenticate][CacheRefreshFailed] CSRF token refresh failed, \"\n \"falling back to full re-authentication: %s\",\n refresh_err,\n )\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full authentication below\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n # Log the payload keys and values (masking password)\n masked_auth = {k: (\"******\" if k == \"password\" else v) for k, v in self.auth.items()}\n app_logger.info(f\"[authenticate][Debug] Login URL: {login_url}\")\n app_logger.info(f\"[authenticate][Debug] Auth payload: {masked_auth}\")\n\n response = self.session.post(login_url, json=self.auth, timeout=self.request_settings[\"timeout\"])\n\n if response.status_code != 200:\n app_logger.error(f\"[authenticate][Error] Status: {response.status_code}, Response: {response.text}\")\n\n response.raise_for_status()\n access_token = response.json()[\"access_token\"]\n\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = self.session.get(csrf_url, headers={\"Authorization\": f\"Bearer {access_token}\"}, timeout=self.request_settings[\"timeout\"])\n csrf_response.raise_for_status()\n\n self._tokens = {\"access_token\": access_token, \"csrf_token\": csrf_response.json()[\"result\"]}\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.reflect(\"Authenticated successfully.\", extra={\"src\": \"authenticate\"})\n return self._tokens\n except requests.exceptions.HTTPError as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = e.response.status_code if e.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable during authentication (Status {status_code})\", status_code=status_code) from e\n raise AuthenticationError(f\"Authentication failed: {e}\") from e\n except (requests.exceptions.RequestException, KeyError) as e:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {e}\") from e\n # #endregion APIClient.authenticate\n @property\n # #region headers [TYPE Function]\n # @PURPOSE: Возвращает HTTP-заголовки для аутентифицированных запросов.\n # @PRE: APIClient is initialized and authenticated or can be authenticated.\n # @POST: Returns headers including auth tokens.\n def headers(self) -> dict[str, str]:\n if not self._authenticated:\n self.authenticate()\n return {\n \"Authorization\": f\"Bearer {self._tokens['access_token']}\",\n \"X-CSRFToken\": self._tokens.get(\"csrf_token\", \"\"),\n \"Referer\": self.base_url,\n \"Content-Type\": \"application/json\"\n }\n # #endregion headers\n # #region request [TYPE Function]\n # @PURPOSE: Выполняет универсальный HTTP-запрос к API.\n # @PARAM: method (str) - HTTP метод.\n # @PARAM: endpoint (str) - API эндпоинт.\n # @PARAM: headers (Optional[Dict]) - Дополнительные заголовки.\n # @PARAM: raw_response (bool) - Возвращать ли сырой ответ.\n # @PRE: method and endpoint must be strings.\n # @POST: Returns response content or raw Response object.\n # @RETURN: `requests.Response` если `raw_response=True`, иначе `dict`.\n # @THROW: SupersetAPIError, NetworkError и их подклассы.\n def request(self, method: str, endpoint: str, headers: dict | None = None, raw_response: bool = False, **kwargs) -> requests.Response | dict[str, Any]:\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n if headers:\n _headers.update(headers)\n\n try:\n response = self.session.request(method, full_url, headers=_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except requests.exceptions.HTTPError as e:\n if e.response is not None and e.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(e, endpoint)\n except requests.exceptions.RequestException as e:\n self._handle_network_error(e, full_url)\n # #endregion request\n # #region _handle_http_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует HTTP ошибки в кастомные исключения.\n # @PARAM: e (requests.exceptions.HTTPError) - Ошибка.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PRE: e must be a valid HTTPError with a response.\n # @POST: Raises a specific SupersetAPIError or subclass.\n def _handle_http_error(self, e: requests.exceptions.HTTPError, endpoint: str):\n with belief_scope(\"_handle_http_error\"):\n status_code = e.response.status_code\n if status_code == 502 or status_code == 503 or status_code == 504:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from e\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from e\n raise SupersetAPIError(\n f\"API resource not found at endpoint '{endpoint}'\",\n status_code=status_code,\n endpoint=endpoint,\n subtype=\"not_found\",\n ) from e\n if status_code == 403:\n raise PermissionDeniedError() from e\n if status_code == 401:\n raise AuthenticationError() from e\n raise SupersetAPIError(f\"API Error {status_code}: {e.response.text}\") from e\n # #endregion _handle_http_error\n # #region _is_dashboard_endpoint [TYPE Function]\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\n # @PRE: endpoint may be relative or absolute.\n # @POST: Returns true only for dashboard-specific endpoints.\n def _is_dashboard_endpoint(self, endpoint: str) -> bool:\n normalized_endpoint = str(endpoint or \"\").strip().lower()\n if not normalized_endpoint:\n return False\n if normalized_endpoint.startswith(\"http://\") or normalized_endpoint.startswith(\"https://\"):\n try:\n normalized_endpoint = \"/\" + normalized_endpoint.split(\"/api/v1\", 1)[1].lstrip(\"/\")\n except IndexError:\n return False\n if normalized_endpoint.startswith(\"/api/v1/\"):\n normalized_endpoint = normalized_endpoint[len(\"/api/v1\"):]\n return normalized_endpoint.startswith(\"/dashboard/\") or normalized_endpoint == \"/dashboard\"\n # #endregion _is_dashboard_endpoint\n # #region _handle_network_error [TYPE Function]\n # @PURPOSE: (Helper) Преобразует сетевые ошибки в `NetworkError`.\n # @PARAM: e (requests.exceptions.RequestException) - Ошибка.\n # @PARAM: url (str) - URL.\n # @PRE: e must be a RequestException.\n # @POST: Raises a NetworkError.\n def _handle_network_error(self, e: requests.exceptions.RequestException, url: str):\n with belief_scope(\"_handle_network_error\"):\n if isinstance(e, requests.exceptions.Timeout):\n msg = \"Request timeout\"\n elif isinstance(e, requests.exceptions.ConnectionError):\n msg = \"Connection error\"\n else:\n msg = f\"Unknown network error: {e}\"\n raise NetworkError(msg, url=url) from e\n # #endregion _handle_network_error\n # #region upload_file [TYPE Function]\n # @PURPOSE: Загружает файл на сервер через multipart/form-data.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: file_info (Dict[str, Any]) - Информация о файле.\n # @PARAM: extra_data (Optional[Dict]) - Дополнительные данные.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: file_info must contain 'file_obj' and 'file_name'.\n # @POST: File is uploaded and response returned.\n # @RETURN: Ответ API в виде словаря.\n # @THROW: SupersetAPIError, NetworkError, TypeError.\n def upload_file(self, endpoint: str, file_info: dict[str, Any], extra_data: dict | None = None, timeout: int | None = None) -> dict:\n with belief_scope(\"upload_file\"):\n full_url = self._build_api_url(endpoint)\n _headers = self.headers.copy()\n _headers.pop('Content-Type', None)\n\n\n file_obj, file_name, form_field = file_info.get(\"file_obj\"), file_info.get(\"file_name\"), file_info.get(\"form_field\", \"file\")\n\n files_payload = {}\n if isinstance(file_obj, (str, Path)):\n with open(file_obj, 'rb') as f:\n files_payload = {form_field: (file_name, f.read(), 'application/x-zip-compressed')}\n elif isinstance(file_obj, io.BytesIO):\n files_payload = {form_field: (file_name, file_obj.getvalue(), 'application/x-zip-compressed')}\n else:\n raise TypeError(f\"Unsupported file_obj type: {type(file_obj)}\")\n\n return self._perform_upload(full_url, files_payload, extra_data, _headers, timeout)\n # #endregion upload_file\n # #region _perform_upload [TYPE Function]\n # @PURPOSE: (Helper) Выполняет POST запрос с файлом.\n # @PARAM: url (str) - URL.\n # @PARAM: files (Dict) - Файлы.\n # @PARAM: data (Optional[Dict]) - Данные.\n # @PARAM: headers (Dict) - Заголовки.\n # @PARAM: timeout (Optional[int]) - Таймаут.\n # @PRE: url, files, and headers must be provided.\n # @POST: POST request is performed and JSON response returned.\n # @RETURN: Dict - Ответ.\n def _perform_upload(self, url: str, files: dict, data: dict | None, headers: dict, timeout: int | None) -> dict:\n with belief_scope(\"_perform_upload\"):\n try:\n response = self.session.post(url, files=files, data=data or {}, headers=headers, timeout=timeout or self.request_settings[\"timeout\"])\n response.raise_for_status()\n if response.status_code == 200:\n try:\n return response.json()\n except Exception as json_e:\n app_logger.debug(f\"[_perform_upload][Debug] Response is not valid JSON: {response.text[:200]}...\")\n raise SupersetAPIError(f\"API error during upload: Response is not valid JSON: {json_e}\") from json_e\n return response.json()\n except requests.exceptions.HTTPError as e:\n raise SupersetAPIError(f\"API error during upload: {e.response.text}\") from e\n except requests.exceptions.RequestException as e:\n raise NetworkError(f\"Network error during upload: {e}\", url=url) from e\n # #endregion _perform_upload\n # #region fetch_paginated_count [TYPE Function]\n # @PURPOSE: Получает общее количество элементов для пагинации.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: query_params (Dict) - Параметры запроса.\n # @PARAM: count_field (str) - Поле с количеством.\n # @PRE: query_params must be a dictionary.\n # @POST: Returns total count of items.\n # @RETURN: int - Количество.\n def fetch_paginated_count(self, endpoint: str, query_params: dict, count_field: str = \"count\") -> int:\n with belief_scope(\"fetch_paginated_count\"):\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query_params)}))\n return response_json.get(count_field, 0)\n # #endregion fetch_paginated_count\n # #region fetch_paginated_data [TYPE Function]\n # @PURPOSE: Автоматически собирает данные со всех страниц пагинированного эндпоинта.\n # @PARAM: endpoint (str) - Эндпоинт.\n # @PARAM: pagination_options (Dict[str, Any]) - Опции пагинации.\n # @PRE: pagination_options must contain 'base_query', 'results_field'. 'total_count' is optional.\n # @POST: Returns all items across all pages.\n # @RETURN: List[Any] - Список данных.\n def fetch_paginated_data(self, endpoint: str, pagination_options: dict[str, Any]) -> list[Any]:\n with belief_scope(\"fetch_paginated_data\"):\n base_query = pagination_options[\"base_query\"]\n total_count = pagination_options.get(\"total_count\")\n\n results_field = pagination_options[\"results_field\"]\n count_field = pagination_options.get(\"count_field\", \"count\")\n page_size = base_query.get('page_size', 1000)\n assert page_size > 0, \"'page_size' must be a positive number.\"\n\n results = []\n page = 0\n\n # Fetch first page to get data and total count if not provided\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n\n first_page_results = response_json.get(results_field, [])\n results.extend(first_page_results)\n\n if total_count is None:\n total_count = response_json.get(count_field, len(first_page_results))\n app_logger.debug(f\"[fetch_paginated_data][State] Total count resolved from first page: {total_count}\")\n # Fetch remaining pages\n total_pages = (total_count + page_size - 1) // page_size\n for page in range(1, total_pages):\n query = {**base_query, 'page': page}\n response_json = cast(dict[str, Any], self.request(\"GET\", endpoint, params={\"q\": json.dumps(query)}))\n results.extend(response_json.get(results_field, []))\n\n return results\n # #endregion fetch_paginated_data\n# #endregion APIClient\n" }, { "contract_id": "APIClient.__init__", @@ -39623,52 +40466,24 @@ "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", "start_line": 214, - "end_line": 224, + "end_line": 229, "tier": "TIER_1", "complexity": 1, "metadata": { - "POST": "Returns canonical base URL suitable for building API endpoints.", - "PRE": "raw_url can be empty.", - "PURPOSE": "Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.", - "RETURN": "str" + "PURPOSE": "Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix." }, "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region _normalize_base_url [TYPE Function]\n # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.\n # @PRE: raw_url can be empty.\n # @POST: Returns canonical base URL suitable for building API endpoints.\n # @RETURN: str\n def _normalize_base_url(self, raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n return normalized.rstrip(\"/\")\n # #endregion _normalize_base_url\n" + "body": " # #region _normalize_base_url [TYPE Function]\n # @PURPOSE: Normalize Superset environment URL to base host/path without trailing slash and /api/v1 suffix.\n # Auto-prepends https:// if no scheme is present.\n # @PRE: raw_url can be empty.\n # @POST: Returns canonical base URL with scheme, suitable for building API endpoints.\n # @RETURN: str\n def _normalize_base_url(self, raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[:-len(\"/api/v1\")]\n normalized = normalized.rstrip(\"/\")\n # Auto-prepend https:// if no scheme is present\n if normalized and not normalized.startswith(\"http://\") and not normalized.startswith(\"https://\"):\n normalized = f\"https://{normalized}\"\n return normalized\n # #endregion _normalize_base_url\n" }, { "contract_id": "_build_api_url", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 225, - "end_line": 239, + "start_line": 230, + "end_line": 244, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39712,8 +40527,8 @@ "contract_id": "APIClient.authenticate", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 240, - "end_line": 324, + "start_line": 245, + "end_line": 329, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39822,8 +40637,8 @@ "contract_id": "headers", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 326, - "end_line": 339, + "start_line": 331, + "end_line": 344, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39860,8 +40675,8 @@ "contract_id": "request", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 340, - "end_line": 368, + "start_line": 345, + "end_line": 373, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39919,8 +40734,8 @@ "contract_id": "_handle_http_error", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 369, - "end_line": 394, + "start_line": 374, + "end_line": 399, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39964,8 +40779,8 @@ "contract_id": "_is_dashboard_endpoint", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 395, - "end_line": 411, + "start_line": 400, + "end_line": 416, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40002,8 +40817,8 @@ "contract_id": "_handle_network_error", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 412, - "end_line": 427, + "start_line": 417, + "end_line": 432, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40047,8 +40862,8 @@ "contract_id": "upload_file", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 428, - "end_line": 457, + "start_line": 433, + "end_line": 462, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40106,8 +40921,8 @@ "contract_id": "_perform_upload", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 458, - "end_line": 484, + "start_line": 463, + "end_line": 489, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40158,8 +40973,8 @@ "contract_id": "fetch_paginated_count", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 485, - "end_line": 497, + "start_line": 490, + "end_line": 502, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40210,8 +41025,8 @@ "contract_id": "fetch_paginated_data", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 498, - "end_line": 536, + "start_line": 503, + "end_line": 541, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42165,14 +42980,16 @@ "contract_type": "Module", "file_path": "backend/src/models/assistant.py", "start_line": 1, - "end_line": 78, + "end_line": 80, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "DATA_CONTRACT": "AssistantData -> AssistantRecord", "INVARIANT": "Assistant records preserve immutable ids and creation timestamps.", "LAYER": "Domain", - "PURPOSE": "SQLAlchemy models for assistant audit trail and confirmation tokens." + "PURPOSE": "SQLAlchemy models for assistant audit trail and confirmation tokens.", + "SIDE_EFFECT": "Defines assistant audit/message/confirmation tables" }, "relations": [ { @@ -42183,6 +43000,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", @@ -42191,18 +43017,27 @@ "actual_complexity": 3, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, assistant, model, schema, audit, assistant-audit-record]\n# @BRIEF SQLAlchemy models for assistant audit trail and confirmation tokens.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @INVARIANT: Assistant records preserve immutable ids and creation timestamps.\n\nfrom datetime import datetime\n\nfrom sqlalchemy import JSON, Column, DateTime, String, Text\n\nfrom .mapping import Base\n\n\n# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE: user_id must identify the actor for every record.\n# @POST: Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantAuditRecord\n\n\n# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE: user_id, conversation_id, role and text must be present.\n# @POST: Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantMessageRecord\n\n\n# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE: intent/dispatch and expiry timestamp must be provided.\n# @POST: State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n# #endregion AssistantModels\n" + "body": "# #region AssistantModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, assistant, model, schema, audit, assistant-audit-record]\n# @BRIEF SQLAlchemy models for assistant audit trail and confirmation tokens.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @INVARIANT: Assistant records preserve immutable ids and creation timestamps.\n# @SIDE_EFFECT: Defines assistant audit/message/confirmation tables\n# @DATA_CONTRACT: AssistantData -> AssistantRecord\n\nfrom datetime import datetime\n\nfrom sqlalchemy import JSON, Column, DateTime, String, Text\n\nfrom .mapping import Base\n\n\n# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE: user_id must identify the actor for every record.\n# @POST: Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantAuditRecord\n\n\n# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE: user_id, conversation_id, role and text must be present.\n# @POST: Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantMessageRecord\n\n\n# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE: intent/dispatch and expiry timestamp must be provided.\n# @POST: State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n# #endregion AssistantModels\n" }, { "contract_id": "AssistantAuditRecord", "contract_type": "Class", "file_path": "backend/src/models/assistant.py", - "start_line": 14, - "end_line": 32, + "start_line": 16, + "end_line": 34, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -42247,8 +43082,8 @@ "contract_id": "AssistantMessageRecord", "contract_type": "Class", "file_path": "backend/src/models/assistant.py", - "start_line": 35, - "end_line": 55, + "start_line": 37, + "end_line": 57, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -42293,8 +43128,8 @@ "contract_id": "AssistantConfirmationRecord", "contract_type": "Class", "file_path": "backend/src/models/assistant.py", - "start_line": 58, - "end_line": 77, + "start_line": 60, + "end_line": 79, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -43823,14 +44658,18 @@ "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", "start_line": 1, - "end_line": 122, + "end_line": 126, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "DATA_CONTRACT": "ClarificationData -> ClarificationRecord", "INVARIANT": "Only one active clarification question may exist at a time per session.", "LAYER": "Domain", - "PURPOSE": "Clarification session, question, option, and answer models for guided review flow." + "POST": "Clarification ORM models registered", + "PRE": "Database engine initialized", + "PURPOSE": "Clarification session, question, option, and answer models for guided review flow.", + "SIDE_EFFECT": "Defines dataset review clarification tables" }, "relations": [ { @@ -43847,6 +44686,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", @@ -43855,18 +44703,45 @@ "actual_complexity": 3, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewClarificationModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, clarification, model]\n# @BRIEF Clarification session, question, option, and answer models for guided review flow.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT: Only one active clarification question may exist at a time per session.\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy import (\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n AnswerKind,\n ClarificationStatus,\n QuestionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n\n\n# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n\n\n# #region ClarificationOption [C:1] [TYPE Class]\n# @BRIEF One selectable option for a clarification question with recommendation flag.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationOption(Base):\n __tablename__ = \"clarification_options\"\n\n option_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False)\n label = Column(String, nullable=False)\n value = Column(String, nullable=False)\n is_recommended = Column(Boolean, nullable=False, default=False)\n display_order = Column(Integer, nullable=False, default=0)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"options\")\n\n\n# #endregion ClarificationOption\n\n\n# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n\n\n# #endregion DatasetReviewClarificationModels\n" + "body": "# #region DatasetReviewClarificationModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, clarification, model]\n# @BRIEF Clarification session, question, option, and answer models for guided review flow.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT: Only one active clarification question may exist at a time per session.\n# @PRE: Database engine initialized\n# @POST: Clarification ORM models registered\n# @SIDE_EFFECT: Defines dataset review clarification tables\n# @DATA_CONTRACT: ClarificationData -> ClarificationRecord\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy import (\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n AnswerKind,\n ClarificationStatus,\n QuestionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n\n\n# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n\n\n# #region ClarificationOption [C:1] [TYPE Class]\n# @BRIEF One selectable option for a clarification question with recommendation flag.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationOption(Base):\n __tablename__ = \"clarification_options\"\n\n option_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False)\n label = Column(String, nullable=False)\n value = Column(String, nullable=False)\n is_recommended = Column(Boolean, nullable=False, default=False)\n display_order = Column(Integer, nullable=False, default=0)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"options\")\n\n\n# #endregion ClarificationOption\n\n\n# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n\n\n# #endregion DatasetReviewClarificationModels\n" }, { "contract_id": "ClarificationSession", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 33, - "end_line": 54, + "start_line": 37, + "end_line": 58, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -43900,8 +44775,8 @@ "contract_id": "ClarificationQuestion", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 57, - "end_line": 79, + "start_line": 61, + "end_line": 83, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -43935,8 +44810,8 @@ "contract_id": "ClarificationOption", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 82, - "end_line": 98, + "start_line": 86, + "end_line": 102, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43970,8 +44845,8 @@ "contract_id": "ClarificationAnswer", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 101, - "end_line": 119, + "start_line": 105, + "end_line": 123, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -45127,14 +46002,18 @@ "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", "start_line": 1, - "end_line": 137, + "end_line": 141, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "DATA_CONTRACT": "SemanticData -> SemanticFieldEntry", "INVARIANT": "Manual overrides are never silently replaced by imported, inferred, or AI-generated values.", "LAYER": "Domain", - "PURPOSE": "Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment." + "POST": "Semantic ORM models registered with override protection", + "PRE": "Database engine initialized", + "PURPOSE": "Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment.", + "SIDE_EFFECT": "Defines dataset review semantic tables" }, "relations": [ { @@ -45151,6 +46030,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", @@ -45159,18 +46047,45 @@ "actual_complexity": 3, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSemanticModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, semantic, enrichment]\n# @BRIEF Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values.\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Float,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy import (\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n CandidateMatchType,\n CandidateStatus,\n FieldKind,\n FieldProvenance,\n SemanticSourceStatus,\n SemanticSourceType,\n TrustLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n\n\n# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT: Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n\n\n# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n\n\n# #endregion DatasetReviewSemanticModels\n" + "body": "# #region DatasetReviewSemanticModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, semantic, enrichment]\n# @BRIEF Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values.\n# @PRE: Database engine initialized\n# @POST: Semantic ORM models registered with override protection\n# @SIDE_EFFECT: Defines dataset review semantic tables\n# @DATA_CONTRACT: SemanticData -> SemanticFieldEntry\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Float,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy import (\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n CandidateMatchType,\n CandidateStatus,\n FieldKind,\n FieldProvenance,\n SemanticSourceStatus,\n SemanticSourceType,\n TrustLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n\n\n# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT: Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n\n\n# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n\n\n# #endregion DatasetReviewSemanticModels\n" }, { "contract_id": "SemanticSource", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", - "start_line": 38, - "end_line": 64, + "start_line": 42, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -45204,8 +46119,8 @@ "contract_id": "SemanticFieldEntry", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", - "start_line": 67, - "end_line": 106, + "start_line": 71, + "end_line": 110, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -45246,8 +46161,8 @@ "contract_id": "SemanticCandidate", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", - "start_line": 109, - "end_line": 134, + "start_line": 113, + "end_line": 138, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -45282,14 +46197,18 @@ "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_session_models.py", "start_line": 1, - "end_line": 155, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "DATA_CONTRACT": "SessionData -> SessionRecord", "INVARIANT": "Session and profile entities are strictly scoped to an authenticated user.", "LAYER": "Domain", - "PURPOSE": "Session aggregate root and collaborator models for dataset review orchestration." + "POST": "Session ORM models registered with optimistic locking", + "PRE": "Database engine initialized", + "PURPOSE": "Session aggregate root and collaborator models for dataset review orchestration.", + "SIDE_EFFECT": "Defines dataset review session tables" }, "relations": [ { @@ -45306,6 +46225,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", @@ -45314,18 +46242,45 @@ "actual_complexity": 3, "contract_type": "Module" } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSessionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, session, model]\n# @BRIEF Session aggregate root and collaborator models for dataset review orchestration.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT: Session and profile entities are strictly scoped to an authenticated user.\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n ForeignKey,\n Integer,\n String,\n)\nfrom sqlalchemy import (\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n SessionPhase,\n SessionStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n\n\n# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT: Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=False)\n source_kind = Column(String, nullable=False)\n source_input = Column(String, nullable=False)\n dataset_ref = Column(String, nullable=False)\n dataset_id = Column(Integer, nullable=True)\n dashboard_id = Column(Integer, nullable=True)\n readiness_state = Column(\n SQLEnum(ReadinessState), nullable=False, default=ReadinessState.EMPTY\n )\n recommended_action = Column(\n SQLEnum(RecommendedAction),\n nullable=False,\n default=RecommendedAction.IMPORT_FROM_SUPERSET,\n )\n version = Column(Integer, nullable=False, default=0)\n __mapper_args__ = {\"version_id_col\": version, \"version_id_generator\": False}\n status = Column(\n SQLEnum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE\n )\n current_phase = Column(\n SQLEnum(SessionPhase), nullable=False, default=SessionPhase.INTAKE\n )\n active_task_id = Column(String, nullable=True)\n last_preview_id = Column(String, nullable=True)\n last_run_context_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n last_activity_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n closed_at = Column(DateTime, nullable=True)\n\n owner = relationship(\"User\")\n collaborators = relationship(\n \"SessionCollaborator\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n profile = relationship(\n \"DatasetProfile\",\n back_populates=\"session\",\n uselist=False,\n cascade=\"all, delete-orphan\",\n )\n findings = relationship(\n \"ValidationFinding\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_sources = relationship(\n \"SemanticSource\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_fields = relationship(\n \"SemanticFieldEntry\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n imported_filters = relationship(\n \"ImportedFilter\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n template_variables = relationship(\n \"TemplateVariable\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n execution_mappings = relationship(\n \"ExecutionMapping\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n clarification_sessions = relationship(\n \"ClarificationSession\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n previews = relationship(\n \"CompiledPreview\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n run_contexts = relationship(\n \"DatasetRunContext\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n export_artifacts = relationship(\n \"ExportArtifact\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n events = relationship(\n \"SessionEvent\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion DatasetReviewSession\n\n\n# #endregion DatasetReviewSessionModels\n" + "body": "# #region DatasetReviewSessionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, session, model]\n# @BRIEF Session aggregate root and collaborator models for dataset review orchestration.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT: Session and profile entities are strictly scoped to an authenticated user.\n# @PRE: Database engine initialized\n# @POST: Session ORM models registered with optimistic locking\n# @SIDE_EFFECT: Defines dataset review session tables\n# @DATA_CONTRACT: SessionData -> SessionRecord\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n ForeignKey,\n Integer,\n String,\n)\nfrom sqlalchemy import (\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n SessionPhase,\n SessionStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n\n\n# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT: Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=False)\n source_kind = Column(String, nullable=False)\n source_input = Column(String, nullable=False)\n dataset_ref = Column(String, nullable=False)\n dataset_id = Column(Integer, nullable=True)\n dashboard_id = Column(Integer, nullable=True)\n readiness_state = Column(\n SQLEnum(ReadinessState), nullable=False, default=ReadinessState.EMPTY\n )\n recommended_action = Column(\n SQLEnum(RecommendedAction),\n nullable=False,\n default=RecommendedAction.IMPORT_FROM_SUPERSET,\n )\n version = Column(Integer, nullable=False, default=0)\n __mapper_args__ = {\"version_id_col\": version, \"version_id_generator\": False}\n status = Column(\n SQLEnum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE\n )\n current_phase = Column(\n SQLEnum(SessionPhase), nullable=False, default=SessionPhase.INTAKE\n )\n active_task_id = Column(String, nullable=True)\n last_preview_id = Column(String, nullable=True)\n last_run_context_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n last_activity_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n closed_at = Column(DateTime, nullable=True)\n\n owner = relationship(\"User\")\n collaborators = relationship(\n \"SessionCollaborator\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n profile = relationship(\n \"DatasetProfile\",\n back_populates=\"session\",\n uselist=False,\n cascade=\"all, delete-orphan\",\n )\n findings = relationship(\n \"ValidationFinding\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_sources = relationship(\n \"SemanticSource\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_fields = relationship(\n \"SemanticFieldEntry\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n imported_filters = relationship(\n \"ImportedFilter\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n template_variables = relationship(\n \"TemplateVariable\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n execution_mappings = relationship(\n \"ExecutionMapping\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n clarification_sessions = relationship(\n \"ClarificationSession\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n previews = relationship(\n \"CompiledPreview\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n run_contexts = relationship(\n \"DatasetRunContext\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n export_artifacts = relationship(\n \"ExportArtifact\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n events = relationship(\n \"SessionEvent\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion DatasetReviewSession\n\n\n# #endregion DatasetReviewSessionModels\n" }, { "contract_id": "SessionCollaborator", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_session_models.py", - "start_line": 33, - "end_line": 51, + "start_line": 37, + "end_line": 55, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -45359,8 +46314,8 @@ "contract_id": "DatasetReviewSession", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_session_models.py", - "start_line": 54, - "end_line": 152, + "start_line": 58, + "end_line": 156, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -46171,15 +47126,19 @@ "contract_type": "Module", "file_path": "backend/src/models/mapping.py", "start_line": 1, - "end_line": 96, - "tier": "TIER_2", - "complexity": 3, + "end_line": 100, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "CONSTRAINT": "source_env_id and target_env_id must be valid environment IDs.", + "DATA_CONTRACT": "MappingData -> MappingRecord", "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", - "PURPOSE": "Defines the database schema for environment metadata and database mappings using SQLAlchemy." + "POST": "Mapping ORM models registered with UUID primary keys", + "PRE": "Database engine initialized", + "PURPOSE": "Defines the database schema for environment metadata and database mappings using SQLAlchemy.", + "SIDE_EFFECT": "Defines environment/database/resource mapping tables" }, "relations": [ { @@ -46195,27 +47154,18 @@ "tag": "CONSTRAINT", "message": "@CONSTRAINT is not defined in axiom_config.yaml tags", "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MappingModels [C:3] [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 -> 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\nimport enum\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, ForeignKey, String\nfrom sqlalchemy import Enum as SQLEnum\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\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\"), 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\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\"), 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\"), 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 -> 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, ForeignKey, String\nfrom sqlalchemy import Enum as SQLEnum\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\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\"), 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\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\"), 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\"), 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": "ResourceType", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 21, - "end_line": 27, + "start_line": 25, + "end_line": 31, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -46232,8 +47182,8 @@ "contract_id": "MigrationStatus", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 30, - "end_line": 38, + "start_line": 34, + "end_line": 42, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -46250,8 +47200,8 @@ "contract_id": "MigrationJob", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 67, - "end_line": 78, + "start_line": 71, + "end_line": 82, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -46268,8 +47218,8 @@ "contract_id": "ResourceMapping", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 80, - "end_line": 94, + "start_line": 84, + "end_line": 98, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -46302,14 +47252,18 @@ "contract_type": "Module", "file_path": "backend/src/models/profile.py", "start_line": 1, - "end_line": 58, - "tier": "TIER_2", - "complexity": 3, + "end_line": 62, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ProfileData -> PreferenceRecord", "INVARIANT": "Sensitive Git token is stored encrypted and never returned in plaintext.", "LAYER": "Domain", - "PURPOSE": "Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences." + "POST": "Profile ORM models registered", + "PRE": "Database engine initialized", + "PURPOSE": "Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences.", + "SIDE_EFFECT": "Defines user profile/preference tables" }, "relations": [ { @@ -46326,15 +47280,6 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -46356,14 +47301,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileModels [C:3] [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_FROM -> [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\nimport uuid\nfrom datetime import datetime\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_FROM -> [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\nimport uuid\nfrom datetime import datetime\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" }, { "contract_id": "UserDashboardPreference", "contract_type": "Class", "file_path": "backend/src/models/profile.py", - "start_line": 20, - "end_line": 56, + "start_line": 24, + "end_line": 60, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47628,12 +48573,13 @@ "contract_type": "Module", "file_path": "backend/src/plugins/git_plugin.py", "start_line": 1, - "end_line": 399, - "tier": "TIER_1", - "complexity": 1, + "end_line": 369, + "tier": "TIER_2", + "complexity": 4, "metadata": { + "COMPLEXITY": 4, "CONSTRAINT": "Плагин работает только с распакованными YAML-экспортами Superset.", - "INVARIANT": "Все операции с Git должны выполняться через GitService.", + "INVARIANT": "_handle_sync сохраняет backup управляемых директорий перед удалением;", "LAYER": "Plugin", "PURPOSE": "Предоставляет плагин для версионирования и развертывания дашбордов Superset." }, @@ -47679,18 +48625,36 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C1", + "message": "@INVARIANT is forbidden for contract type 'Module' at C4", "detail": { - "actual_complexity": 1, + "actual_complexity": 4, "contract_type": "Module" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", "detail": { - "actual_complexity": 1, + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, "contract_type": "Module" } }, @@ -47787,14 +48751,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitPluginModule [TYPE Module] [SEMANTICS git, versioning, deploy, sync, superset]\n#\n# @BRIEF Предоставляет плагин для версионирования и развертывания дашбордов Superset.\n# @LAYER: Plugin\n# @RELATION INHERITS_FROM -> src.core.plugin_base.PluginBase\n# @RELATION USES -> src.services.git_service.GitService\n# @RELATION USES -> src.core.superset_client.SupersetClient\n# @RELATION USES -> src.core.config_manager.ConfigManager\n# @RELATION USES -> TaskContext\n#\n# @INVARIANT: Все операции с Git должны выполняться через GitService.\n# @CONSTRAINT: Плагин работает только с распакованными YAML-экспортами Superset.\n\nimport io\nimport os\nimport shutil\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope\nfrom src.core.logger import 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: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n app_logger.info(f\"GitPlugin initialized with {config_path}\")\n # endregion __init__\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the plugin identifier.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns 'git-integration'.\n def id(self) -> str:\n with belief_scope(\"GitPlugin.id\"):\n return \"git-integration\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the plugin name.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the human-readable name.\n def name(self) -> str:\n with belief_scope(\"GitPlugin.name\"):\n return \"Git Integration\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns the plugin description.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the plugin's purpose description.\n def description(self) -> str:\n with belief_scope(\"GitPlugin.description\"):\n return \"Version control for Superset dashboards\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the plugin version.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the version string.\n def version(self) -> str:\n with belief_scope(\"GitPlugin.version\"):\n return \"0.1.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the git plugin.\n # @RETURN: str - \"/git\"\n def ui_route(self) -> str:\n with belief_scope(\"GitPlugin.ui_route\"):\n return \"/git\"\n # 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 [TYPE Function]\n # @PURPOSE: Выполняет начальную настройку плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Плагин готов к выполнению задач.\n async def initialize(self):\n with belief_scope(\"GitPlugin.initialize\"):\n app_logger.reason(\"Initializing Git Integration Plugin logic.\", extra={\"src\": \"GitPlugin.initialize\"})\n\n # region execute [TYPE Function]\n # @PURPOSE: Основной метод выполнения задач плагина с поддержкой TaskContext.\n # @PRE: task_data содержит 'operation' и 'dashboard_id'.\n # @POST: Возвращает результат выполнения операции.\n # @PARAM: task_data (Dict[str, Any]) - Данные задачи.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @RETURN: Dict[str, Any] - Статус и сообщение.\n # @RELATION: CALLS -> self._handle_sync\n # @RELATION: CALLS -> self._handle_deploy\n async def execute(self, task_data: dict[str, Any], context: 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 [TYPE Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: str | None = None, log=None, git_log=None, superset_log=None) -> dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n\n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n\n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n\n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n\n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n\n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n\n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n\n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n 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 [TYPE Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n\n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n\n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n\n zip_buffer.seek(0)\n\n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n\n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n\n try:\n 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 [TYPE Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: 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 # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n app_logger.reflect(f\"Found environment in DB: {db_env.name}\", extra={\"src\": \"_get_env\"})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n\n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n app_logger.reflect(f\"Found environment {env_id} in ConfigManager list\", extra={\"src\": \"_get_env\"})\n return env\n\n if 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 initialize\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_FROM -> src.core.plugin_base.PluginBase\n# @RELATION USES -> src.services.git_service.GitService\n# @RELATION USES -> src.core.superset_client.SupersetClient\n# @RELATION USES -> src.core.config_manager.ConfigManager\n# @RELATION USES -> TaskContext\n#\n# @INVARIANT: Все операции с Git должны выполняться через GitService.\n# @CONSTRAINT: Плагин работает только с распакованными YAML-экспортами Superset.\n# @INVARIANT: _handle_sync сохраняет backup управляемых директорий перед удалением;\n# при ошибке распаковки backup восстанавливается.\n\nimport io\nimport os\nimport shutil\nimport time\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope\nfrom src.core.logger import 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: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n app_logger.info(f\"GitPlugin initialized with {config_path}\")\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", "contract_type": "Class", "file_path": "backend/src/plugins/git_plugin.py", - "start_line": 30, - "end_line": 398, + "start_line": 33, + "end_line": 368, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -47804,7 +48768,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitPlugin [TYPE Class]\n# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n app_logger.info(f\"GitPlugin initialized with {config_path}\")\n # endregion __init__\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the plugin identifier.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns 'git-integration'.\n def id(self) -> str:\n with belief_scope(\"GitPlugin.id\"):\n return \"git-integration\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the plugin name.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the human-readable name.\n def name(self) -> str:\n with belief_scope(\"GitPlugin.name\"):\n return \"Git Integration\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns the plugin description.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the plugin's purpose description.\n def description(self) -> str:\n with belief_scope(\"GitPlugin.description\"):\n return \"Version control for Superset dashboards\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the plugin version.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the version string.\n def version(self) -> str:\n with belief_scope(\"GitPlugin.version\"):\n return \"0.1.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the git plugin.\n # @RETURN: str - \"/git\"\n def ui_route(self) -> str:\n with belief_scope(\"GitPlugin.ui_route\"):\n return \"/git\"\n # 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 [TYPE Function]\n # @PURPOSE: Выполняет начальную настройку плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Плагин готов к выполнению задач.\n async def initialize(self):\n with belief_scope(\"GitPlugin.initialize\"):\n app_logger.reason(\"Initializing Git Integration Plugin logic.\", extra={\"src\": \"GitPlugin.initialize\"})\n\n # region execute [TYPE Function]\n # @PURPOSE: Основной метод выполнения задач плагина с поддержкой TaskContext.\n # @PRE: task_data содержит 'operation' и 'dashboard_id'.\n # @POST: Возвращает результат выполнения операции.\n # @PARAM: task_data (Dict[str, Any]) - Данные задачи.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @RETURN: Dict[str, Any] - Статус и сообщение.\n # @RELATION: CALLS -> self._handle_sync\n # @RELATION: CALLS -> self._handle_deploy\n async def execute(self, task_data: dict[str, Any], context: 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 [TYPE Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: str | None = None, log=None, git_log=None, superset_log=None) -> dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n\n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n\n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n\n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n\n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n\n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n\n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n\n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n 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 [TYPE Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n\n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n\n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n\n zip_buffer.seek(0)\n\n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n\n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n\n try:\n 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 [TYPE Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: 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 # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n app_logger.reflect(f\"Found environment in DB: {db_env.name}\", extra={\"src\": \"_get_env\"})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n\n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n app_logger.reflect(f\"Found environment {env_id} in ConfigManager list\", extra={\"src\": \"_get_env\"})\n return env\n\n if 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 initialize\n# #endregion GitPlugin\n" + "body": "# #region GitPlugin [TYPE Class]\n# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n app_logger.info(f\"GitPlugin initialized with {config_path}\")\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" }, { "contract_id": "LLMAnalysisPackage", @@ -47937,13 +48901,14 @@ "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/plugin.py", "start_line": 1, - "end_line": 481, - "tier": "TIER_2", - "complexity": 3, + "end_line": 482, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "AnalysisRequest -> AnalysisResult", "INVARIANT": "All LLM interactions must be executed as asynchronous tasks.", - "LAYER": "Domain", + "LAYER": "Plugin", "PURPOSE": "Implements DashboardValidationPlugin and DocumentationPlugin." }, "relations": [ @@ -47980,11 +48945,29 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } }, @@ -48009,14 +48992,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation]\n# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin.\n# @LAYER: Domain\n# @RELATION INHERITS -> [PluginBase]\n# @RELATION CALLS -> [ScreenshotService]\n# @RELATION CALLS -> [LLMClient]\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION USES -> TaskContext\n# @INVARIANT: All LLM interactions must be executed as asynchronous tasks.\n\nimport json\nimport os\nfrom datetime import datetime, timedelta\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 is_multimodal_model,\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 -> 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 if not is_multimodal_model(db_provider.default_model, db_provider.provider_type):\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 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 -> 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 USES -> TaskContext\n# @INVARIANT: All LLM interactions must be executed as asynchronous tasks.\n# @DATA_CONTRACT: AnalysisRequest -> AnalysisResult\n\nimport json\nimport os\nfrom datetime import datetime, timedelta\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 is_multimodal_model,\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 -> 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 if not is_multimodal_model(db_provider.default_model, db_provider.provider_type):\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 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 -> 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" }, { "contract_id": "_is_masked_or_invalid_api_key", "contract_type": "Function", "file_path": "backend/src/plugins/llm_analysis/plugin.py", - "start_line": 34, - "end_line": 46, + "start_line": 35, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -48053,8 +49036,8 @@ "contract_id": "_json_safe_value", "contract_type": "Function", "file_path": "backend/src/plugins/llm_analysis/plugin.py", - "start_line": 48, - "end_line": 60, + "start_line": 49, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -48091,8 +49074,8 @@ "contract_id": "DashboardValidationPlugin", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/plugin.py", - "start_line": 62, - "end_line": 323, + "start_line": 63, + "end_line": 324, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -48125,8 +49108,8 @@ "contract_id": "DocumentationPlugin", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/plugin.py", - "start_line": 325, - "end_line": 479, + "start_line": 326, + "end_line": 480, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -48231,13 +49214,14 @@ "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/service.py", "start_line": 1, - "end_line": 981, - "tier": "TIER_2", - "complexity": 3, + "end_line": 982, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "DashboardSpec -> Screenshot + Analysis", "INVARIANT": "Screenshots must be 1920px width and capture full page height.", - "LAYER": "Domain", + "LAYER": "Plugin", "PURPOSE": "Services for LLM interaction and dashboard screenshots." }, "relations": [ @@ -48262,25 +49246,43 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMAnalysisService [C:3] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]\n# @BRIEF Services for LLM interaction and dashboard screenshots.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> playwright\n# @RELATION DEPENDS_ON -> openai\n# @RELATION DEPENDS_ON -> tenacity\n# @INVARIANT: Screenshots must be 1920px width and capture full page height.\n\nimport asyncio\nimport base64\nimport io\nimport json\nimport os\nfrom typing import Any\nfrom urllib.parse import urlsplit\n\nimport httpx\nfrom openai import AsyncOpenAI, RateLimitError\nfrom openai import AuthenticationError as OpenAIAuthenticationError\nfrom PIL import Image\nfrom playwright.async_api import async_playwright\nfrom tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential\n\nfrom ...core.config_models import Environment\nfrom ...core.logger import belief_scope, logger\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\nfrom .models import LLMProviderType\n\n\n# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> list[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> dict[str, str]:\n hidden_fields: dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=10000,\n fail_on_status_code=False,\n max_redirects=0,\n )\n response_url = str(getattr(response, \"url\", \"\") or \"\")\n response_status = int(getattr(response, \"status\", 0) or 0)\n response_headers = dict(getattr(response, \"headers\", {}) or {})\n redirect_location = str(\n response_headers.get(\"location\")\n or response_headers.get(\"Location\")\n or \"\"\n ).strip()\n redirect_statuses = {301, 302, 303, 307, 308}\n if response_status in redirect_statuses:\n redirect_authenticated = self._redirect_looks_authenticated(redirect_location)\n logger.info(\n f\"[DEBUG] Direct form login fallback redirect response: status={response_status} url={response_url} location={redirect_location!r} authenticated={redirect_authenticated}\"\n )\n return redirect_authenticated\n\n response_text = await response.text()\n text_snippet = \" \".join(response_text.split())[:200]\n looks_like_login_page = self._response_looks_like_login_page(response_text)\n logger.info(\n f\"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}\"\n )\n return not looks_like_login_page\n # endregion ScreenshotService._submit_login_via_form_post\n\n # region ScreenshotService._find_login_field_locator [TYPE Function]\n # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.\n # @PRE: field_name is `username` or `password`.\n # @POST: Returns a locator for the corresponding input or None.\n async def _find_login_field_locator(self, page, field_name: str) -> Any:\n normalized = str(field_name or \"\").strip().lower()\n for root in self._iter_login_roots(page):\n if normalized == \"username\":\n input_candidates = [\n root.get_by_label(\"Username\", exact=False),\n root.get_by_label(\"Login\", exact=False),\n root.locator(\"label:text-matches('Username|Login', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Username|Login/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='username']\"),\n root.locator(\"input#username\"),\n root.locator(\"input[placeholder*='Username']\"),\n root.locator(\"input[type='text']\"),\n root.locator(\"input:not([type='password'])\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n if normalized == \"password\":\n input_candidates = [\n root.get_by_label(\"Password\", exact=False),\n root.locator(\"label:text-matches('Password', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Password/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='password']\"),\n root.locator(\"input#password\"),\n root.locator(\"input[placeholder*='Password']\"),\n root.locator(\"input[type='password']\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n return None\n # endregion ScreenshotService._find_login_field_locator\n\n # region ScreenshotService._find_submit_locator [TYPE Function]\n # @PURPOSE: Resolve login submit button from main page or embedded auth frame.\n # @PRE: page is ready for login interaction.\n # @POST: Returns visible submit locator or None.\n async def _find_submit_locator(self, page) -> Any:\n selectors = [\n lambda root: root.get_by_role(\"button\", name=\"Sign in\", exact=False),\n lambda root: root.get_by_role(\"button\", name=\"Login\", exact=False),\n lambda root: root.locator(\"button[type='submit']\"),\n lambda root: root.locator(\"button#submit\"),\n lambda root: root.locator(\".btn-primary\"),\n lambda root: root.locator(\"input[type='submit']\"),\n ]\n for root in self._iter_login_roots(page):\n locator = await self._find_first_visible_locator([factory(root) for factory in selectors])\n if locator:\n return locator\n return None\n # endregion ScreenshotService._find_submit_locator\n\n # region ScreenshotService._goto_resilient [TYPE Function]\n # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.\n # @PRE: page is a valid Playwright page and url is non-empty.\n # @POST: Returns last navigation response or raises when both primary and fallback waits fail.\n async def _goto_resilient(\n self,\n page,\n url: str,\n primary_wait_until: str = \"domcontentloaded\",\n fallback_wait_until: str = \"load\",\n timeout: int = 60000,\n ):\n try:\n return await page.goto(url, wait_until=primary_wait_until, timeout=timeout)\n except Exception as primary_error:\n logger.warning(\n f\"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}\"\n )\n return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)\n # endregion ScreenshotService._goto_resilient\n\n # region ScreenshotService.capture_dashboard [TYPE Function]\n # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.\n # @PRE: dashboard_id is a valid string, output_path is a writable path.\n # @POST: Returns True if screenshot is saved successfully.\n # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n with belief_scope(\"capture_dashboard\", f\"dashboard_id={dashboard_id}\"):\n logger.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n async with async_playwright() as p:\n browser = await p.chromium.launch(\n headless=True,\n args=[\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--no-sandbox\"\n ]\n )\n # Set a realistic user agent to avoid 403 Forbidden from OpenResty/WAF\n user_agent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n # Construct base UI URL from environment (strip /api/v1 suffix)\n base_ui_url = self.env.url.rstrip(\"/\")\n if base_ui_url.endswith(\"/api/v1\"):\n base_ui_url = base_ui_url[:-len(\"/api/v1\")]\n\n # Create browser context with realistic headers\n context = await browser.new_context(\n viewport={'width': 1280, 'height': 720},\n user_agent=user_agent,\n extra_http_headers={\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n \"Accept-Language\": \"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\"\n }\n )\n logger.info(\"Browser context created successfully\")\n\n page = await context.new_page()\n # Bypass navigator.webdriver detection\n await page.add_init_script(\"delete Object.getPrototypeOf(navigator).webdriver\")\n\n # 1. Navigate to login page and authenticate\n login_url = f\"{base_ui_url.rstrip('/')}/login/\"\n logger.info(f\"[DEBUG] Navigating to login page: {login_url}\")\n\n response = await self._goto_resilient(\n page,\n login_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n if response:\n logger.info(f\"[DEBUG] Login page response status: {response.status}\")\n\n # Wait for login form to be ready\n await page.wait_for_load_state(\"domcontentloaded\")\n\n # More exhaustive list of selectors for various Superset versions/themes\n selectors = {\n \"username\": ['input[name=\"username\"]', 'input#username', 'input[placeholder*=\"Username\"]', 'input[type=\"text\"]'],\n \"password\": ['input[name=\"password\"]', 'input#password', 'input[placeholder*=\"Password\"]', 'input[type=\"password\"]'],\n \"submit\": ['button[type=\"submit\"]', 'button#submit', '.btn-primary', 'input[type=\"submit\"]']\n }\n logger.info(\"[DEBUG] Attempting to find login form elements...\")\n\n try:\n used_direct_form_login = False\n # Find and fill username\n username_locator = await self._find_login_field_locator(page, \"username\")\n\n if not username_locator:\n roots = self._iter_login_roots(page)\n logger.info(f\"[DEBUG] Found {len(roots)} login roots including child frames\")\n for root_index, root in enumerate(roots[:5]):\n all_inputs = await root.locator('input').all()\n logger.info(f\"[DEBUG] Root {root_index}: found {len(all_inputs)} input fields\")\n for i, inp in enumerate(all_inputs[:5]): # Log first 5\n inp_type = await inp.get_attribute('type')\n inp_name = await inp.get_attribute('name')\n inp_id = await inp.get_attribute('id')\n logger.info(f\"[DEBUG] Root {root_index} input {i}: type={inp_type}, name={inp_name}, id={inp_id}\")\n used_direct_form_login = await self._submit_login_via_form_post(page, login_url)\n if not used_direct_form_login:\n raise RuntimeError(\"Could not find username input field on login page\")\n username_locator = None\n\n if username_locator is not None:\n logger.info(\"[DEBUG] Filling username field\")\n await username_locator.fill(self.env.username)\n\n # Find and fill password\n password_locator = await self._find_login_field_locator(page, \"password\") if username_locator is not None else None\n\n if username_locator is not None and not password_locator:\n raise RuntimeError(\"Could not find password input field on login page\")\n\n if password_locator is not None:\n logger.info(\"[DEBUG] Filling password field\")\n await password_locator.fill(self.env.password)\n\n # Click submit\n submit_locator = await self._find_submit_locator(page) if username_locator is not None else None\n\n if username_locator is not None and not submit_locator:\n raise RuntimeError(\"Could not find submit button on login page\")\n\n if submit_locator is not None:\n logger.info(\"[DEBUG] Clicking submit button\")\n await submit_locator.click()\n\n # Wait for navigation after login\n if not used_direct_form_login:\n try:\n await page.wait_for_load_state(\"load\", timeout=30000)\n except Exception as load_wait_error:\n logger.warning(f\"[DEBUG] Login post-submit load wait timed out: {load_wait_error}\")\n\n # Check if login was successful\n if not used_direct_form_login and \"/login\" in page.url:\n # Check for error messages on page\n error_msg = await page.locator(\".alert-danger, .error-message\").text_content() if await page.locator(\".alert-danger, .error-message\").count() > 0 else \"Unknown error\"\n logger.error(f\"[DEBUG] Login failed. Still on login page. Error: {error_msg}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {error_msg}. Debug screenshot saved to {debug_path}\")\n\n logger.info(f\"[DEBUG] Login successful. Current URL: {page.url}\")\n\n # Check cookies after successful login\n page_cookies = await context.cookies()\n logger.info(f\"[DEBUG] Cookies after login: {len(page_cookies)}\")\n for c in page_cookies:\n logger.info(f\"[DEBUG] Cookie: name={c['name']}, domain={c['domain']}, value={c.get('value', '')[:20]}...\")\n\n except Exception as e:\n page_title = await page.title()\n logger.error(f\"UI Login failed. Page title: {page_title}, URL: {page.url}, Error: {e!s}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {e!s}. Debug screenshot saved to {debug_path}\")\n\n # 2. Navigate to dashboard\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n dashboard_url = f\"{base_ui_url.rstrip('/')}/superset/dashboard/{dashboard_id}/?standalone=true\"\n\n if base_ui_url.startswith(\"https://\") and dashboard_url.startswith(\"http://\"):\n dashboard_url = dashboard_url.replace(\"http://\", \"https://\")\n\n logger.info(f\"[DEBUG] Navigating to dashboard: {dashboard_url}\")\n\n # Dashboard pages can keep polling/network activity open indefinitely.\n response = await self._goto_resilient(\n page,\n dashboard_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n\n if response:\n logger.info(f\"[DEBUG] Dashboard navigation response status: {response.status}, URL: {response.url}\")\n\n if \"/login\" in page.url:\n debug_path = output_path.replace(\".png\", \"_debug_failed_dashboard_auth.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(\n f\"Dashboard navigation redirected to login page after authentication. Debug screenshot saved to {debug_path}\"\n )\n\n try:\n # Wait for the dashboard grid to be present\n await page.wait_for_selector('.dashboard-component, .dashboard-header, [data-test=\"dashboard-grid\"]', timeout=30000)\n logger.info(\"[DEBUG] Dashboard container loaded\")\n\n # Wait for charts to finish loading (Superset uses loading spinners/skeletons)\n # We wait until loading indicators disappear or a timeout occurs\n try:\n # Wait for loading indicators to disappear\n await page.wait_for_selector('.loading, .ant-skeleton, .spinner', state=\"hidden\", timeout=60000)\n logger.info(\"[DEBUG] Loading indicators hidden\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for loading indicators to hide\")\n\n # Wait for charts to actually render their content (e.g., ECharts, NVD3)\n # We look for common chart containers that should have content\n try:\n await page.wait_for_selector('.chart-container canvas, .slice_container svg, .superset-chart-canvas, .grid-content .chart-container', timeout=60000)\n logger.info(\"[DEBUG] Chart content detected\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for chart content\")\n\n # Additional check: wait for all chart containers to have non-empty content\n logger.info(\"[DEBUG] Waiting for all charts to have rendered content...\")\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container, .slice_container');\n if (charts.length === 0) return true; // No charts to wait for\n \n // Check if all charts have rendered content (canvas, svg, or non-empty div)\n return Array.from(charts).every(chart => {\n const hasCanvas = chart.querySelector('canvas') !== null;\n const hasSvg = chart.querySelector('svg') !== null;\n const hasContent = chart.innerText.trim().length > 0 || chart.children.length > 0;\n return hasCanvas || hasSvg || hasContent;\n });\n }\"\"\", timeout=60000)\n logger.info(\"[DEBUG] All charts have rendered content\")\n\n # Scroll to bottom and back to top to trigger lazy loading of all charts\n logger.info(\"[DEBUG] Scrolling to trigger lazy loading...\")\n await page.evaluate(\"\"\"async () => {\n const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n for (let i = 0; i < document.body.scrollHeight; i += 500) {\n window.scrollTo(0, i);\n await delay(100);\n }\n window.scrollTo(0, 0);\n await delay(500);\n }\"\"\")\n\n except Exception as e:\n logger.warning(f\"[DEBUG] Dashboard content wait failed: {e}, proceeding anyway after delay\")\n\n # Final stabilization delay - increased for complex dashboards\n logger.info(\"[DEBUG] Final stabilization delay...\")\n await asyncio.sleep(15)\n\n # Logic to handle tabs and full-page capture\n try:\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n processed_tabs = set()\n\n async def switch_tabs(depth=0):\n if depth > 3:\n return # Limit recursion depth\n\n tab_selectors = [\n '.ant-tabs-nav-list .ant-tabs-tab',\n '.dashboard-component-tabs .ant-tabs-tab',\n '[data-test=\"dashboard-component-tabs\"] .ant-tabs-tab'\n ]\n\n found_tabs = []\n for selector in tab_selectors:\n found_tabs = await page.locator(selector).all()\n if found_tabs:\n break\n\n if found_tabs:\n logger.info(f\"[DEBUG][TabSwitching] Found {len(found_tabs)} tabs at depth {depth}\")\n for i, tab in enumerate(found_tabs):\n try:\n tab_text = (await tab.inner_text()).strip()\n tab_id = f\"{depth}_{i}_{tab_text}\"\n\n if tab_id in processed_tabs:\n continue\n\n if await tab.is_visible():\n logger.info(f\"[DEBUG][TabSwitching] Switching to tab: {tab_text}\")\n processed_tabs.add(tab_id)\n\n is_active = \"ant-tabs-tab-active\" in (await tab.get_attribute(\"class\") or \"\")\n if not is_active:\n await tab.click()\n await asyncio.sleep(2) # Wait for content to render\n\n await switch_tabs(depth + 1)\n except Exception as tab_e:\n logger.warning(f\"[DEBUG][TabSwitching] Failed to process tab {i}: {tab_e}\")\n\n try:\n first_tab = found_tabs[0]\n if \"ant-tabs-tab-active\" not in (await first_tab.get_attribute(\"class\") or \"\"):\n await first_tab.click()\n await asyncio.sleep(1)\n except Exception:\n pass\n\n await switch_tabs()\n\n # 2. Calculate full height for screenshot\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n full_height = await page.evaluate(\"\"\"() => {\n const body = document.body;\n const html = document.documentElement;\n const dashboardContent = document.querySelector('.dashboard-content');\n \n return Math.max(\n body.scrollHeight, body.offsetHeight,\n html.clientHeight, html.scrollHeight, html.offsetHeight,\n dashboardContent ? dashboardContent.scrollHeight + 100 : 0\n );\n }\"\"\")\n logger.info(f\"[DEBUG] Calculated full height: {full_height}\")\n\n # DIAGNOSTIC: Count chart elements before resize\n chart_count_before = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements BEFORE viewport resize: {chart_count_before}\")\n\n # DIAGNOSTIC: Capture pre-resize screenshot for comparison\n pre_resize_path = output_path.replace(\".png\", \"_preresize.png\")\n try:\n await page.screenshot(path=pre_resize_path, full_page=False, timeout=10000)\n import os\n pre_resize_size = os.path.getsize(pre_resize_path) if os.path.exists(pre_resize_path) else 0\n logger.info(f\"[DIAGNOSTIC] Pre-resize screenshot saved: {pre_resize_path} ({pre_resize_size} bytes)\")\n except Exception as pre_e:\n logger.warning(f\"[DIAGNOSTIC] Failed to capture pre-resize screenshot: {pre_e}\")\n\n logger.info(f\"[DIAGNOSTIC] Resizing viewport from current to 1920x{int(full_height)}\")\n await page.set_viewport_size({\"width\": 1920, \"height\": int(full_height)})\n\n # DIAGNOSTIC: Increased wait time and log timing\n logger.info(\"[DIAGNOSTIC] Waiting 10 seconds after viewport resize for re-render...\")\n await asyncio.sleep(10)\n logger.info(\"[DIAGNOSTIC] Wait completed\")\n\n # DIAGNOSTIC: Count chart elements after resize and wait\n chart_count_after = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements AFTER viewport resize + wait: {chart_count_after}\")\n\n # DIAGNOSTIC: Check if any charts have error states\n chart_errors = await page.evaluate(\"\"\"() => {\n const errors = [];\n document.querySelectorAll('.chart-container, .slice_container').forEach((chart, i) => {\n const errorEl = chart.querySelector('.error, .alert-danger, .ant-alert-error');\n if (errorEl) {\n errors.push({index: i, text: errorEl.innerText.substring(0, 100)});\n }\n });\n return errors;\n }\"\"\")\n if chart_errors:\n logger.warning(f\"[DIAGNOSTIC] Charts with error states detected: {chart_errors}\")\n else:\n logger.info(\"[DIAGNOSTIC] No chart error states detected\")\n\n # 3. Take screenshot using CDP to bypass Playwright's font loading wait\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n logger.info(\"[DEBUG] Attempting full-page screenshot via CDP...\")\n cdp = await page.context.new_cdp_session(page)\n\n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n\n image_data = base64.b64decode(screenshot_data[\"data\"])\n\n with open(output_path, 'wb') as f:\n f.write(image_data)\n\n # DIAGNOSTIC: Verify screenshot file\n import os\n final_size = os.path.getsize(output_path) if os.path.exists(output_path) else 0\n logger.info(f\"[DIAGNOSTIC] Final screenshot saved: {output_path}\")\n logger.info(f\"[DIAGNOSTIC] Final screenshot size: {final_size} bytes ({final_size / 1024:.2f} KB)\")\n\n # DIAGNOSTIC: Get image dimensions\n try:\n with Image.open(output_path) as final_img:\n logger.info(f\"[DIAGNOSTIC] Final screenshot dimensions: {final_img.width}x{final_img.height}\")\n except Exception as img_err:\n logger.warning(f\"[DIAGNOSTIC] Could not read final image dimensions: {img_err}\")\n\n logger.info(f\"Full-page screenshot saved to {output_path} (via CDP)\")\n except Exception as e:\n logger.error(f\"[DEBUG] Full-page/Tab capture failed: {e}\")\n try:\n await page.screenshot(path=output_path, full_page=True, timeout=10000)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=5000)\n\n await browser.close()\n return True\n # endregion ScreenshotService.capture_dashboard\n# #endregion ScreenshotService\n\n# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n\n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n # LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.\n # It routes to upstream providers transparently, and the default Authorization header\n # is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)\n self.client = AsyncOpenAI(\n api_key=self.api_key,\n base_url=base_url,\n default_headers=default_headers,\n http_client=http_client,\n )\n # endregion LLMClient.__init__\n\n # region LLMClient._supports_json_response_format [TYPE Function]\n # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.\n # @PRE: Client initialized with base_url and default_model.\n # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.\n def _supports_json_response_format(self) -> bool:\n base = (self.base_url or \"\").lower()\n model = (self.default_model or \"\").lower()\n\n # OpenRouter routes to many upstream providers; some models reject json_object mode.\n if \"openrouter.ai\" in base:\n incompatible_tokens = (\n \"stepfun/\",\n \"step-\",\n \":free\",\n )\n if any(token in model for token in incompatible_tokens):\n return False\n return True\n # endregion LLMClient._supports_json_response_format\n\n # region LLMClient.get_json_completion [TYPE Function]\n # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.\n # @PRE: messages is a list of valid message dictionaries.\n # @POST: Returns a parsed JSON dictionary.\n # @SIDE_EFFECT: Calls external LLM API.\n def _should_retry(exception: Exception) -> bool:\n \"\"\"Custom retry predicate that excludes authentication errors.\"\"\"\n # Don't retry on authentication errors\n if isinstance(exception, OpenAIAuthenticationError):\n return False\n # Retry on rate limit errors and other exceptions\n return isinstance(exception, (RateLimitError, Exception))\n\n @retry(\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=5, max=60),\n retry=retry_if_exception(_should_retry),\n reraise=True\n )\n async def get_json_completion(self, messages: list[dict[str, Any]]) -> dict[str, Any]:\n with belief_scope(\"get_json_completion\"):\n response = None\n try:\n use_json_mode = self._supports_json_response_format()\n try:\n logger.info(\n f\"[get_json_completion] Attempting LLM call for model: {self.default_model} \"\n f\"(json_mode={'on' if use_json_mode else 'off'})\"\n )\n logger.info(f\"[get_json_completion] Base URL being used: {self.base_url}\")\n logger.info(f\"[get_json_completion] Number of messages: {len(messages)}\")\n logger.info(f\"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}\")\n\n if use_json_mode:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages,\n response_format={\"type\": \"json_object\"}\n )\n else:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n except Exception as e:\n if use_json_mode and (\n \"JSON mode is not enabled\" in str(e)\n or \"json_object is not supported\" in str(e).lower()\n or \"response_format\" in str(e).lower()\n or \"400\" in str(e)\n ):\n logger.warning(f\"[get_json_completion] JSON mode failed or not supported: {e!s}. Falling back to plain text response.\")\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n else:\n raise e\n\n logger.debug(f\"[get_json_completion] LLM Response: {response}\")\n except OpenAIAuthenticationError as e:\n logger.error(f\"[get_json_completion] Authentication error: {e!s}\")\n # Do not retry on auth errors - re-raise to stop retry\n raise\n except RateLimitError as e:\n logger.warning(f\"[get_json_completion] Rate limit hit: {e!s}\")\n\n # Extract retry_delay from error metadata if available\n retry_delay = 5.0 # Default fallback\n try:\n # Based on logs, the raw response is in e.body or e.response.json()\n # The logs show 'metadata': {'raw': '...'} which suggests a proxy or specific client wrapper\n # Let's try to find the 'retryDelay' in the error message or response\n import re\n\n # Try to find \"retryDelay\": \"XXs\" in the string representation of the error\n error_str = str(e)\n match = re.search(r'\"retryDelay\":\\s*\"(\\d+)s\"', error_str)\n if match:\n retry_delay = float(match.group(1))\n else:\n # Try to parse from response if it's a standard OpenAI-like error with body\n if hasattr(e, 'body') and isinstance(e.body, dict):\n # Some providers put it in details\n details = e.body.get('error', {}).get('details', [])\n for detail in details:\n if detail.get('@type') == 'type.googleapis.com/google.rpc.RetryInfo':\n delay_str = detail.get('retryDelay', '5s')\n retry_delay = float(delay_str.rstrip('s'))\n break\n except Exception as parse_e:\n logger.debug(f\"[get_json_completion] Failed to parse retry delay: {parse_e}\")\n\n # Add a small safety margin (0.5s) as requested\n wait_time = retry_delay + 0.5\n logger.info(f\"[get_json_completion] Waiting for {wait_time}s before retry...\")\n await asyncio.sleep(wait_time)\n raise\n except Exception as e:\n logger.error(f\"[get_json_completion] LLM call failed: {e!s}\")\n raise\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n raise RuntimeError(f\"Invalid LLM response: {response}\")\n\n content = response.choices[0].message.content\n logger.debug(f\"[get_json_completion] Raw content to parse: {content}\")\n\n try:\n return json.loads(content)\n except json.JSONDecodeError:\n logger.warning(\"[get_json_completion] Failed to parse JSON directly, attempting to extract from code blocks\")\n if \"```json\" in content:\n json_str = content.split(\"```json\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n elif \"```\" in content:\n json_str = content.split(\"```\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n else:\n raise\n # endregion LLMClient.get_json_completion\n\n # region LLMClient.test_runtime_connection [TYPE Function]\n # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.\n # @PRE: Client is initialized with provider credentials and default_model.\n # @POST: Returns lightweight JSON payload when runtime auth/model path is valid.\n # @SIDE_EFFECT: Calls external LLM API.\n async def test_runtime_connection(self) -> dict[str, Any]:\n with belief_scope(\"test_runtime_connection\"):\n messages = [\n {\n \"role\": \"user\",\n \"content\": 'Return exactly this JSON object and nothing else: {\"ok\": true}',\n }\n ]\n return await self.get_json_completion(messages)\n # endregion LLMClient.test_runtime_connection\n\n # region LLMClient.fetch_models [TYPE Function]\n # @PURPOSE: Fetch available models from the provider's API.\n # @PRE: Client is initialized with provider credentials.\n # @POST: Returns a list of model ID strings.\n # @SIDE_EFFECT: Calls external LLM API /v1/models endpoint.\n async def fetch_models(self) -> list[str]:\n with belief_scope(\"LLMClient.fetch_models\"):\n try:\n response = await self.client.models.list()\n model_ids = [m.id for m in response.data]\n model_ids.sort()\n logger.reason(\n f\"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}\",\n extra={\"src\": \"LLMClient.fetch_models\"},\n )\n return model_ids\n except Exception as e:\n logger.warning(\n f\"[LLMClient.fetch_models] Failed to fetch models: {e}\",\n )\n raise\n # endregion LLMClient.fetch_models\n\n # region LLMClient.analyze_dashboard [TYPE Function]\n # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.\n # @PRE: screenshot_path exists, logs is a list of strings.\n # @POST: Returns a structured analysis dictionary (status, summary, issues).\n # @SIDE_EFFECT: Reads screenshot file and calls external LLM API.\n async def analyze_dashboard(\n self,\n screenshot_path: str,\n logs: list[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n ) -> dict[str, Any]:\n with belief_scope(\"analyze_dashboard\"):\n # Optimize image to reduce token count (US1 / T023)\n # Gemini/Gemma models have limits on input tokens, and large images contribute significantly.\n try:\n with Image.open(screenshot_path) as img:\n # Convert to RGB if necessary\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n\n # Resize if too large (max 1024px width while maintaining aspect ratio)\n # We reduce width further to 1024px to stay within token limits for long dashboards\n max_width = 1024\n if img.width > max_width or img.height > 2048:\n # Calculate scaling factor to fit within 1024x2048\n scale = min(max_width / img.width, 2048 / img.height)\n if scale < 1.0:\n new_width = int(img.width * scale)\n new_height = int(img.height * scale)\n img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)\n logger.info(f\"[analyze_dashboard] Resized image from {img.width}x{img.height} to {new_width}x{new_height}\")\n\n # Compress and convert to base64\n buffer = io.BytesIO()\n # Lower quality to 60% to further reduce payload size\n img.save(buffer, format=\"JPEG\", quality=60, optimize=True)\n base_64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')\n logger.info(f\"[analyze_dashboard] Optimized image size: {len(buffer.getvalue()) / 1024:.2f} KB\")\n except Exception as img_e:\n logger.warning(f\"[analyze_dashboard] Image optimization failed: {img_e}. Using raw image.\")\n with open(screenshot_path, \"rb\") as image_file:\n base_64_image = base64.b64encode(image_file.read()).decode('utf-8')\n\n log_text = \"\\n\".join(logs)\n prompt = render_prompt(prompt_template, {\"logs\": log_text})\n\n messages = [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": f\"data:image/jpeg;base64,{base_64_image}\"\n }\n }\n ]\n }\n ]\n\n try:\n return await self.get_json_completion(messages)\n except Exception as e:\n logger.error(f\"[analyze_dashboard] Failed to get analysis: {e!s}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {e!s}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # endregion LLMClient.analyze_dashboard\n# #endregion LLMClient\n\n# #endregion LLMAnalysisService\n" + "body": "# #region LLMAnalysisService [C:5] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]\n# @BRIEF Services for LLM interaction and dashboard screenshots.\n# @LAYER: Plugin\n# @RELATION DEPENDS_ON -> playwright\n# @RELATION DEPENDS_ON -> openai\n# @RELATION DEPENDS_ON -> tenacity\n# @INVARIANT: Screenshots must be 1920px width and capture full page height.\n# @DATA_CONTRACT: DashboardSpec -> Screenshot + Analysis\n\nimport asyncio\nimport base64\nimport io\nimport json\nimport os\nfrom typing import Any\nfrom urllib.parse import urlsplit\n\nimport httpx\nfrom openai import AsyncOpenAI, RateLimitError\nfrom openai import AuthenticationError as OpenAIAuthenticationError\nfrom PIL import Image\nfrom playwright.async_api import async_playwright\nfrom tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential\n\nfrom ...core.config_models import Environment\nfrom ...core.logger import belief_scope, logger\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\nfrom .models import LLMProviderType\n\n\n# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> list[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> dict[str, str]:\n hidden_fields: dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=10000,\n fail_on_status_code=False,\n max_redirects=0,\n )\n response_url = str(getattr(response, \"url\", \"\") or \"\")\n response_status = int(getattr(response, \"status\", 0) or 0)\n response_headers = dict(getattr(response, \"headers\", {}) or {})\n redirect_location = str(\n response_headers.get(\"location\")\n or response_headers.get(\"Location\")\n or \"\"\n ).strip()\n redirect_statuses = {301, 302, 303, 307, 308}\n if response_status in redirect_statuses:\n redirect_authenticated = self._redirect_looks_authenticated(redirect_location)\n logger.info(\n f\"[DEBUG] Direct form login fallback redirect response: status={response_status} url={response_url} location={redirect_location!r} authenticated={redirect_authenticated}\"\n )\n return redirect_authenticated\n\n response_text = await response.text()\n text_snippet = \" \".join(response_text.split())[:200]\n looks_like_login_page = self._response_looks_like_login_page(response_text)\n logger.info(\n f\"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}\"\n )\n return not looks_like_login_page\n # endregion ScreenshotService._submit_login_via_form_post\n\n # region ScreenshotService._find_login_field_locator [TYPE Function]\n # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.\n # @PRE: field_name is `username` or `password`.\n # @POST: Returns a locator for the corresponding input or None.\n async def _find_login_field_locator(self, page, field_name: str) -> Any:\n normalized = str(field_name or \"\").strip().lower()\n for root in self._iter_login_roots(page):\n if normalized == \"username\":\n input_candidates = [\n root.get_by_label(\"Username\", exact=False),\n root.get_by_label(\"Login\", exact=False),\n root.locator(\"label:text-matches('Username|Login', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Username|Login/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='username']\"),\n root.locator(\"input#username\"),\n root.locator(\"input[placeholder*='Username']\"),\n root.locator(\"input[type='text']\"),\n root.locator(\"input:not([type='password'])\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n if normalized == \"password\":\n input_candidates = [\n root.get_by_label(\"Password\", exact=False),\n root.locator(\"label:text-matches('Password', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Password/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='password']\"),\n root.locator(\"input#password\"),\n root.locator(\"input[placeholder*='Password']\"),\n root.locator(\"input[type='password']\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n return None\n # endregion ScreenshotService._find_login_field_locator\n\n # region ScreenshotService._find_submit_locator [TYPE Function]\n # @PURPOSE: Resolve login submit button from main page or embedded auth frame.\n # @PRE: page is ready for login interaction.\n # @POST: Returns visible submit locator or None.\n async def _find_submit_locator(self, page) -> Any:\n selectors = [\n lambda root: root.get_by_role(\"button\", name=\"Sign in\", exact=False),\n lambda root: root.get_by_role(\"button\", name=\"Login\", exact=False),\n lambda root: root.locator(\"button[type='submit']\"),\n lambda root: root.locator(\"button#submit\"),\n lambda root: root.locator(\".btn-primary\"),\n lambda root: root.locator(\"input[type='submit']\"),\n ]\n for root in self._iter_login_roots(page):\n locator = await self._find_first_visible_locator([factory(root) for factory in selectors])\n if locator:\n return locator\n return None\n # endregion ScreenshotService._find_submit_locator\n\n # region ScreenshotService._goto_resilient [TYPE Function]\n # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.\n # @PRE: page is a valid Playwright page and url is non-empty.\n # @POST: Returns last navigation response or raises when both primary and fallback waits fail.\n async def _goto_resilient(\n self,\n page,\n url: str,\n primary_wait_until: str = \"domcontentloaded\",\n fallback_wait_until: str = \"load\",\n timeout: int = 60000,\n ):\n try:\n return await page.goto(url, wait_until=primary_wait_until, timeout=timeout)\n except Exception as primary_error:\n logger.warning(\n f\"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}\"\n )\n return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)\n # endregion ScreenshotService._goto_resilient\n\n # region ScreenshotService.capture_dashboard [TYPE Function]\n # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.\n # @PRE: dashboard_id is a valid string, output_path is a writable path.\n # @POST: Returns True if screenshot is saved successfully.\n # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n with belief_scope(\"capture_dashboard\", f\"dashboard_id={dashboard_id}\"):\n logger.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n async with async_playwright() as p:\n browser = await p.chromium.launch(\n headless=True,\n args=[\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--no-sandbox\"\n ]\n )\n # Set a realistic user agent to avoid 403 Forbidden from OpenResty/WAF\n user_agent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n # Construct base UI URL from environment (strip /api/v1 suffix)\n base_ui_url = self.env.url.rstrip(\"/\")\n if base_ui_url.endswith(\"/api/v1\"):\n base_ui_url = base_ui_url[:-len(\"/api/v1\")]\n\n # Create browser context with realistic headers\n context = await browser.new_context(\n viewport={'width': 1280, 'height': 720},\n user_agent=user_agent,\n extra_http_headers={\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n \"Accept-Language\": \"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\"\n }\n )\n logger.info(\"Browser context created successfully\")\n\n page = await context.new_page()\n # Bypass navigator.webdriver detection\n await page.add_init_script(\"delete Object.getPrototypeOf(navigator).webdriver\")\n\n # 1. Navigate to login page and authenticate\n login_url = f\"{base_ui_url.rstrip('/')}/login/\"\n logger.info(f\"[DEBUG] Navigating to login page: {login_url}\")\n\n response = await self._goto_resilient(\n page,\n login_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n if response:\n logger.info(f\"[DEBUG] Login page response status: {response.status}\")\n\n # Wait for login form to be ready\n await page.wait_for_load_state(\"domcontentloaded\")\n\n # More exhaustive list of selectors for various Superset versions/themes\n selectors = {\n \"username\": ['input[name=\"username\"]', 'input#username', 'input[placeholder*=\"Username\"]', 'input[type=\"text\"]'],\n \"password\": ['input[name=\"password\"]', 'input#password', 'input[placeholder*=\"Password\"]', 'input[type=\"password\"]'],\n \"submit\": ['button[type=\"submit\"]', 'button#submit', '.btn-primary', 'input[type=\"submit\"]']\n }\n logger.info(\"[DEBUG] Attempting to find login form elements...\")\n\n try:\n used_direct_form_login = False\n # Find and fill username\n username_locator = await self._find_login_field_locator(page, \"username\")\n\n if not username_locator:\n roots = self._iter_login_roots(page)\n logger.info(f\"[DEBUG] Found {len(roots)} login roots including child frames\")\n for root_index, root in enumerate(roots[:5]):\n all_inputs = await root.locator('input').all()\n logger.info(f\"[DEBUG] Root {root_index}: found {len(all_inputs)} input fields\")\n for i, inp in enumerate(all_inputs[:5]): # Log first 5\n inp_type = await inp.get_attribute('type')\n inp_name = await inp.get_attribute('name')\n inp_id = await inp.get_attribute('id')\n logger.info(f\"[DEBUG] Root {root_index} input {i}: type={inp_type}, name={inp_name}, id={inp_id}\")\n used_direct_form_login = await self._submit_login_via_form_post(page, login_url)\n if not used_direct_form_login:\n raise RuntimeError(\"Could not find username input field on login page\")\n username_locator = None\n\n if username_locator is not None:\n logger.info(\"[DEBUG] Filling username field\")\n await username_locator.fill(self.env.username)\n\n # Find and fill password\n password_locator = await self._find_login_field_locator(page, \"password\") if username_locator is not None else None\n\n if username_locator is not None and not password_locator:\n raise RuntimeError(\"Could not find password input field on login page\")\n\n if password_locator is not None:\n logger.info(\"[DEBUG] Filling password field\")\n await password_locator.fill(self.env.password)\n\n # Click submit\n submit_locator = await self._find_submit_locator(page) if username_locator is not None else None\n\n if username_locator is not None and not submit_locator:\n raise RuntimeError(\"Could not find submit button on login page\")\n\n if submit_locator is not None:\n logger.info(\"[DEBUG] Clicking submit button\")\n await submit_locator.click()\n\n # Wait for navigation after login\n if not used_direct_form_login:\n try:\n await page.wait_for_load_state(\"load\", timeout=30000)\n except Exception as load_wait_error:\n logger.warning(f\"[DEBUG] Login post-submit load wait timed out: {load_wait_error}\")\n\n # Check if login was successful\n if not used_direct_form_login and \"/login\" in page.url:\n # Check for error messages on page\n error_msg = await page.locator(\".alert-danger, .error-message\").text_content() if await page.locator(\".alert-danger, .error-message\").count() > 0 else \"Unknown error\"\n logger.error(f\"[DEBUG] Login failed. Still on login page. Error: {error_msg}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {error_msg}. Debug screenshot saved to {debug_path}\")\n\n logger.info(f\"[DEBUG] Login successful. Current URL: {page.url}\")\n\n # Check cookies after successful login\n page_cookies = await context.cookies()\n logger.info(f\"[DEBUG] Cookies after login: {len(page_cookies)}\")\n for c in page_cookies:\n logger.info(f\"[DEBUG] Cookie: name={c['name']}, domain={c['domain']}, value={c.get('value', '')[:20]}...\")\n\n except Exception as e:\n page_title = await page.title()\n logger.error(f\"UI Login failed. Page title: {page_title}, URL: {page.url}, Error: {e!s}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {e!s}. Debug screenshot saved to {debug_path}\")\n\n # 2. Navigate to dashboard\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n dashboard_url = f\"{base_ui_url.rstrip('/')}/superset/dashboard/{dashboard_id}/?standalone=true\"\n\n if base_ui_url.startswith(\"https://\") and dashboard_url.startswith(\"http://\"):\n dashboard_url = dashboard_url.replace(\"http://\", \"https://\")\n\n logger.info(f\"[DEBUG] Navigating to dashboard: {dashboard_url}\")\n\n # Dashboard pages can keep polling/network activity open indefinitely.\n response = await self._goto_resilient(\n page,\n dashboard_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n\n if response:\n logger.info(f\"[DEBUG] Dashboard navigation response status: {response.status}, URL: {response.url}\")\n\n if \"/login\" in page.url:\n debug_path = output_path.replace(\".png\", \"_debug_failed_dashboard_auth.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(\n f\"Dashboard navigation redirected to login page after authentication. Debug screenshot saved to {debug_path}\"\n )\n\n try:\n # Wait for the dashboard grid to be present\n await page.wait_for_selector('.dashboard-component, .dashboard-header, [data-test=\"dashboard-grid\"]', timeout=30000)\n logger.info(\"[DEBUG] Dashboard container loaded\")\n\n # Wait for charts to finish loading (Superset uses loading spinners/skeletons)\n # We wait until loading indicators disappear or a timeout occurs\n try:\n # Wait for loading indicators to disappear\n await page.wait_for_selector('.loading, .ant-skeleton, .spinner', state=\"hidden\", timeout=60000)\n logger.info(\"[DEBUG] Loading indicators hidden\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for loading indicators to hide\")\n\n # Wait for charts to actually render their content (e.g., ECharts, NVD3)\n # We look for common chart containers that should have content\n try:\n await page.wait_for_selector('.chart-container canvas, .slice_container svg, .superset-chart-canvas, .grid-content .chart-container', timeout=60000)\n logger.info(\"[DEBUG] Chart content detected\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for chart content\")\n\n # Additional check: wait for all chart containers to have non-empty content\n logger.info(\"[DEBUG] Waiting for all charts to have rendered content...\")\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container, .slice_container');\n if (charts.length === 0) return true; // No charts to wait for\n \n // Check if all charts have rendered content (canvas, svg, or non-empty div)\n return Array.from(charts).every(chart => {\n const hasCanvas = chart.querySelector('canvas') !== null;\n const hasSvg = chart.querySelector('svg') !== null;\n const hasContent = chart.innerText.trim().length > 0 || chart.children.length > 0;\n return hasCanvas || hasSvg || hasContent;\n });\n }\"\"\", timeout=60000)\n logger.info(\"[DEBUG] All charts have rendered content\")\n\n # Scroll to bottom and back to top to trigger lazy loading of all charts\n logger.info(\"[DEBUG] Scrolling to trigger lazy loading...\")\n await page.evaluate(\"\"\"async () => {\n const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n for (let i = 0; i < document.body.scrollHeight; i += 500) {\n window.scrollTo(0, i);\n await delay(100);\n }\n window.scrollTo(0, 0);\n await delay(500);\n }\"\"\")\n\n except Exception as e:\n logger.warning(f\"[DEBUG] Dashboard content wait failed: {e}, proceeding anyway after delay\")\n\n # Final stabilization delay - increased for complex dashboards\n logger.info(\"[DEBUG] Final stabilization delay...\")\n await asyncio.sleep(15)\n\n # Logic to handle tabs and full-page capture\n try:\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n processed_tabs = set()\n\n async def switch_tabs(depth=0):\n if depth > 3:\n return # Limit recursion depth\n\n tab_selectors = [\n '.ant-tabs-nav-list .ant-tabs-tab',\n '.dashboard-component-tabs .ant-tabs-tab',\n '[data-test=\"dashboard-component-tabs\"] .ant-tabs-tab'\n ]\n\n found_tabs = []\n for selector in tab_selectors:\n found_tabs = await page.locator(selector).all()\n if found_tabs:\n break\n\n if found_tabs:\n logger.info(f\"[DEBUG][TabSwitching] Found {len(found_tabs)} tabs at depth {depth}\")\n for i, tab in enumerate(found_tabs):\n try:\n tab_text = (await tab.inner_text()).strip()\n tab_id = f\"{depth}_{i}_{tab_text}\"\n\n if tab_id in processed_tabs:\n continue\n\n if await tab.is_visible():\n logger.info(f\"[DEBUG][TabSwitching] Switching to tab: {tab_text}\")\n processed_tabs.add(tab_id)\n\n is_active = \"ant-tabs-tab-active\" in (await tab.get_attribute(\"class\") or \"\")\n if not is_active:\n await tab.click()\n await asyncio.sleep(2) # Wait for content to render\n\n await switch_tabs(depth + 1)\n except Exception as tab_e:\n logger.warning(f\"[DEBUG][TabSwitching] Failed to process tab {i}: {tab_e}\")\n\n try:\n first_tab = found_tabs[0]\n if \"ant-tabs-tab-active\" not in (await first_tab.get_attribute(\"class\") or \"\"):\n await first_tab.click()\n await asyncio.sleep(1)\n except Exception:\n pass\n\n await switch_tabs()\n\n # 2. Calculate full height for screenshot\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n full_height = await page.evaluate(\"\"\"() => {\n const body = document.body;\n const html = document.documentElement;\n const dashboardContent = document.querySelector('.dashboard-content');\n \n return Math.max(\n body.scrollHeight, body.offsetHeight,\n html.clientHeight, html.scrollHeight, html.offsetHeight,\n dashboardContent ? dashboardContent.scrollHeight + 100 : 0\n );\n }\"\"\")\n logger.info(f\"[DEBUG] Calculated full height: {full_height}\")\n\n # DIAGNOSTIC: Count chart elements before resize\n chart_count_before = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements BEFORE viewport resize: {chart_count_before}\")\n\n # DIAGNOSTIC: Capture pre-resize screenshot for comparison\n pre_resize_path = output_path.replace(\".png\", \"_preresize.png\")\n try:\n await page.screenshot(path=pre_resize_path, full_page=False, timeout=10000)\n import os\n pre_resize_size = os.path.getsize(pre_resize_path) if os.path.exists(pre_resize_path) else 0\n logger.info(f\"[DIAGNOSTIC] Pre-resize screenshot saved: {pre_resize_path} ({pre_resize_size} bytes)\")\n except Exception as pre_e:\n logger.warning(f\"[DIAGNOSTIC] Failed to capture pre-resize screenshot: {pre_e}\")\n\n logger.info(f\"[DIAGNOSTIC] Resizing viewport from current to 1920x{int(full_height)}\")\n await page.set_viewport_size({\"width\": 1920, \"height\": int(full_height)})\n\n # DIAGNOSTIC: Increased wait time and log timing\n logger.info(\"[DIAGNOSTIC] Waiting 10 seconds after viewport resize for re-render...\")\n await asyncio.sleep(10)\n logger.info(\"[DIAGNOSTIC] Wait completed\")\n\n # DIAGNOSTIC: Count chart elements after resize and wait\n chart_count_after = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements AFTER viewport resize + wait: {chart_count_after}\")\n\n # DIAGNOSTIC: Check if any charts have error states\n chart_errors = await page.evaluate(\"\"\"() => {\n const errors = [];\n document.querySelectorAll('.chart-container, .slice_container').forEach((chart, i) => {\n const errorEl = chart.querySelector('.error, .alert-danger, .ant-alert-error');\n if (errorEl) {\n errors.push({index: i, text: errorEl.innerText.substring(0, 100)});\n }\n });\n return errors;\n }\"\"\")\n if chart_errors:\n logger.warning(f\"[DIAGNOSTIC] Charts with error states detected: {chart_errors}\")\n else:\n logger.info(\"[DIAGNOSTIC] No chart error states detected\")\n\n # 3. Take screenshot using CDP to bypass Playwright's font loading wait\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n logger.info(\"[DEBUG] Attempting full-page screenshot via CDP...\")\n cdp = await page.context.new_cdp_session(page)\n\n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n\n image_data = base64.b64decode(screenshot_data[\"data\"])\n\n with open(output_path, 'wb') as f:\n f.write(image_data)\n\n # DIAGNOSTIC: Verify screenshot file\n import os\n final_size = os.path.getsize(output_path) if os.path.exists(output_path) else 0\n logger.info(f\"[DIAGNOSTIC] Final screenshot saved: {output_path}\")\n logger.info(f\"[DIAGNOSTIC] Final screenshot size: {final_size} bytes ({final_size / 1024:.2f} KB)\")\n\n # DIAGNOSTIC: Get image dimensions\n try:\n with Image.open(output_path) as final_img:\n logger.info(f\"[DIAGNOSTIC] Final screenshot dimensions: {final_img.width}x{final_img.height}\")\n except Exception as img_err:\n logger.warning(f\"[DIAGNOSTIC] Could not read final image dimensions: {img_err}\")\n\n logger.info(f\"Full-page screenshot saved to {output_path} (via CDP)\")\n except Exception as e:\n logger.error(f\"[DEBUG] Full-page/Tab capture failed: {e}\")\n try:\n await page.screenshot(path=output_path, full_page=True, timeout=10000)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=5000)\n\n await browser.close()\n return True\n # endregion ScreenshotService.capture_dashboard\n# #endregion ScreenshotService\n\n# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n\n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n # LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.\n # It routes to upstream providers transparently, and the default Authorization header\n # is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)\n self.client = AsyncOpenAI(\n api_key=self.api_key,\n base_url=base_url,\n default_headers=default_headers,\n http_client=http_client,\n )\n # endregion LLMClient.__init__\n\n # region LLMClient._supports_json_response_format [TYPE Function]\n # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.\n # @PRE: Client initialized with base_url and default_model.\n # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.\n def _supports_json_response_format(self) -> bool:\n base = (self.base_url or \"\").lower()\n model = (self.default_model or \"\").lower()\n\n # OpenRouter routes to many upstream providers; some models reject json_object mode.\n if \"openrouter.ai\" in base:\n incompatible_tokens = (\n \"stepfun/\",\n \"step-\",\n \":free\",\n )\n if any(token in model for token in incompatible_tokens):\n return False\n return True\n # endregion LLMClient._supports_json_response_format\n\n # region LLMClient.get_json_completion [TYPE Function]\n # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.\n # @PRE: messages is a list of valid message dictionaries.\n # @POST: Returns a parsed JSON dictionary.\n # @SIDE_EFFECT: Calls external LLM API.\n def _should_retry(exception: Exception) -> bool:\n \"\"\"Custom retry predicate that excludes authentication errors.\"\"\"\n # Don't retry on authentication errors\n if isinstance(exception, OpenAIAuthenticationError):\n return False\n # Retry on rate limit errors and other exceptions\n return isinstance(exception, (RateLimitError, Exception))\n\n @retry(\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=5, max=60),\n retry=retry_if_exception(_should_retry),\n reraise=True\n )\n async def get_json_completion(self, messages: list[dict[str, Any]]) -> dict[str, Any]:\n with belief_scope(\"get_json_completion\"):\n response = None\n try:\n use_json_mode = self._supports_json_response_format()\n try:\n logger.info(\n f\"[get_json_completion] Attempting LLM call for model: {self.default_model} \"\n f\"(json_mode={'on' if use_json_mode else 'off'})\"\n )\n logger.info(f\"[get_json_completion] Base URL being used: {self.base_url}\")\n logger.info(f\"[get_json_completion] Number of messages: {len(messages)}\")\n logger.info(f\"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}\")\n\n if use_json_mode:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages,\n response_format={\"type\": \"json_object\"}\n )\n else:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n except Exception as e:\n if use_json_mode and (\n \"JSON mode is not enabled\" in str(e)\n or \"json_object is not supported\" in str(e).lower()\n or \"response_format\" in str(e).lower()\n or \"400\" in str(e)\n ):\n logger.warning(f\"[get_json_completion] JSON mode failed or not supported: {e!s}. Falling back to plain text response.\")\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n else:\n raise e\n\n logger.debug(f\"[get_json_completion] LLM Response: {response}\")\n except OpenAIAuthenticationError as e:\n logger.error(f\"[get_json_completion] Authentication error: {e!s}\")\n # Do not retry on auth errors - re-raise to stop retry\n raise\n except RateLimitError as e:\n logger.warning(f\"[get_json_completion] Rate limit hit: {e!s}\")\n\n # Extract retry_delay from error metadata if available\n retry_delay = 5.0 # Default fallback\n try:\n # Based on logs, the raw response is in e.body or e.response.json()\n # The logs show 'metadata': {'raw': '...'} which suggests a proxy or specific client wrapper\n # Let's try to find the 'retryDelay' in the error message or response\n import re\n\n # Try to find \"retryDelay\": \"XXs\" in the string representation of the error\n error_str = str(e)\n match = re.search(r'\"retryDelay\":\\s*\"(\\d+)s\"', error_str)\n if match:\n retry_delay = float(match.group(1))\n else:\n # Try to parse from response if it's a standard OpenAI-like error with body\n if hasattr(e, 'body') and isinstance(e.body, dict):\n # Some providers put it in details\n details = e.body.get('error', {}).get('details', [])\n for detail in details:\n if detail.get('@type') == 'type.googleapis.com/google.rpc.RetryInfo':\n delay_str = detail.get('retryDelay', '5s')\n retry_delay = float(delay_str.rstrip('s'))\n break\n except Exception as parse_e:\n logger.debug(f\"[get_json_completion] Failed to parse retry delay: {parse_e}\")\n\n # Add a small safety margin (0.5s) as requested\n wait_time = retry_delay + 0.5\n logger.info(f\"[get_json_completion] Waiting for {wait_time}s before retry...\")\n await asyncio.sleep(wait_time)\n raise\n except Exception as e:\n logger.error(f\"[get_json_completion] LLM call failed: {e!s}\")\n raise\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n raise RuntimeError(f\"Invalid LLM response: {response}\")\n\n content = response.choices[0].message.content\n logger.debug(f\"[get_json_completion] Raw content to parse: {content}\")\n\n try:\n return json.loads(content)\n except json.JSONDecodeError:\n logger.warning(\"[get_json_completion] Failed to parse JSON directly, attempting to extract from code blocks\")\n if \"```json\" in content:\n json_str = content.split(\"```json\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n elif \"```\" in content:\n json_str = content.split(\"```\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n else:\n raise\n # endregion LLMClient.get_json_completion\n\n # region LLMClient.test_runtime_connection [TYPE Function]\n # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.\n # @PRE: Client is initialized with provider credentials and default_model.\n # @POST: Returns lightweight JSON payload when runtime auth/model path is valid.\n # @SIDE_EFFECT: Calls external LLM API.\n async def test_runtime_connection(self) -> dict[str, Any]:\n with belief_scope(\"test_runtime_connection\"):\n messages = [\n {\n \"role\": \"user\",\n \"content\": 'Return exactly this JSON object and nothing else: {\"ok\": true}',\n }\n ]\n return await self.get_json_completion(messages)\n # endregion LLMClient.test_runtime_connection\n\n # region LLMClient.fetch_models [TYPE Function]\n # @PURPOSE: Fetch available models from the provider's API.\n # @PRE: Client is initialized with provider credentials.\n # @POST: Returns a list of model ID strings.\n # @SIDE_EFFECT: Calls external LLM API /v1/models endpoint.\n async def fetch_models(self) -> list[str]:\n with belief_scope(\"LLMClient.fetch_models\"):\n try:\n response = await self.client.models.list()\n model_ids = [m.id for m in response.data]\n model_ids.sort()\n logger.reason(\n f\"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}\",\n extra={\"src\": \"LLMClient.fetch_models\"},\n )\n return model_ids\n except Exception as e:\n logger.warning(\n f\"[LLMClient.fetch_models] Failed to fetch models: {e}\",\n )\n raise\n # endregion LLMClient.fetch_models\n\n # region LLMClient.analyze_dashboard [TYPE Function]\n # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.\n # @PRE: screenshot_path exists, logs is a list of strings.\n # @POST: Returns a structured analysis dictionary (status, summary, issues).\n # @SIDE_EFFECT: Reads screenshot file and calls external LLM API.\n async def analyze_dashboard(\n self,\n screenshot_path: str,\n logs: list[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n ) -> dict[str, Any]:\n with belief_scope(\"analyze_dashboard\"):\n # Optimize image to reduce token count (US1 / T023)\n # Gemini/Gemma models have limits on input tokens, and large images contribute significantly.\n try:\n with Image.open(screenshot_path) as img:\n # Convert to RGB if necessary\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n\n # Resize if too large (max 1024px width while maintaining aspect ratio)\n # We reduce width further to 1024px to stay within token limits for long dashboards\n max_width = 1024\n if img.width > max_width or img.height > 2048:\n # Calculate scaling factor to fit within 1024x2048\n scale = min(max_width / img.width, 2048 / img.height)\n if scale < 1.0:\n new_width = int(img.width * scale)\n new_height = int(img.height * scale)\n img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)\n logger.info(f\"[analyze_dashboard] Resized image from {img.width}x{img.height} to {new_width}x{new_height}\")\n\n # Compress and convert to base64\n buffer = io.BytesIO()\n # Lower quality to 60% to further reduce payload size\n img.save(buffer, format=\"JPEG\", quality=60, optimize=True)\n base_64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')\n logger.info(f\"[analyze_dashboard] Optimized image size: {len(buffer.getvalue()) / 1024:.2f} KB\")\n except Exception as img_e:\n logger.warning(f\"[analyze_dashboard] Image optimization failed: {img_e}. Using raw image.\")\n with open(screenshot_path, \"rb\") as image_file:\n base_64_image = base64.b64encode(image_file.read()).decode('utf-8')\n\n log_text = \"\\n\".join(logs)\n prompt = render_prompt(prompt_template, {\"logs\": log_text})\n\n messages = [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": f\"data:image/jpeg;base64,{base_64_image}\"\n }\n }\n ]\n }\n ]\n\n try:\n return await self.get_json_completion(messages)\n except Exception as e:\n logger.error(f\"[analyze_dashboard] Failed to get analysis: {e!s}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {e!s}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # endregion LLMClient.analyze_dashboard\n# #endregion LLMClient\n\n# #endregion LLMAnalysisService\n" }, { "contract_id": "ScreenshotService", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 30, - "end_line": 677, + "start_line": 31, + "end_line": 678, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -48296,8 +49298,8 @@ "contract_id": "LLMClient", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 679, - "end_line": 979, + "start_line": 680, + "end_line": 980, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -48774,7 +49776,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestOrchestratorInsertFlow [TYPE Class]\n# @BRIEF Verify the orchestrator's _generate_and_insert_sql produces correct SQL for ClickHouse.\nclass TestOrchestratorInsertFlow:\n \"\"\"Verify TranslationOrchestrator._generate_and_insert_sql with mocked DB.\"\"\"\n\n # region test_generate_and_insert_sql_with_timestamps [TYPE Function]\n # @BRIEF Orchestrator builds correct rows_for_sql from source_data with timestamps.\n def test_generate_and_insert_sql_with_timestamps(self) -> None:\n \"\"\"Verify the orchestrator correctly passes source_data through to SQLGenerator.\"\"\"\n from src.plugins.translate.orchestrator import TranslationOrchestrator\n\n # Mock DB session\n db = MagicMock()\n config_manager = MagicMock()\n\n # Create mock records with timestamp source_data\n rec1 = _make_mock_record(\n target_sql=\"Translated comment 1\",\n report_date=\"1726358400000.0\",\n document_number=\"DOC-001\",\n )\n rec2 = _make_mock_record(\n target_sql=\"Translated comment 2\",\n report_date=\"1726444800000.0\",\n document_number=\"DOC-002\",\n )\n\n # Mock the DB query to return our records\n # Note: orchestrator uses .options(joinedload()) before .filter()\n mock_query = MagicMock()\n mock_query.options.return_value.filter.return_value.all.return_value = [rec1, rec2]\n db.query.return_value = mock_query\n\n # Create mock job\n job = _make_mock_job()\n\n # Create mock run\n run = MagicMock()\n run.id = \"run-test-001\"\n run.job_id = job.id\n\n # Create orchestrator\n orch = TranslationOrchestrator(db, config_manager, \"test-user\")\n\n # Mock the event_log and SupersetSqlLabExecutor\n with patch.object(orch, \"event_log\") as mock_event_log, patch(\n \"src.plugins.translate.orchestrator.SupersetSqlLabExecutor\"\n ) as MockExecutor:\n mock_executor = MagicMock()\n MockExecutor.return_value = mock_executor\n mock_executor.resolve_database_id.return_value = None\n mock_executor.get_database_backend.return_value = None\n mock_executor.execute_and_poll.return_value = {\n \"status\": \"success\",\n \"query_id\": \"q-123\",\n \"rows_affected\": 2,\n }\n\n # Call _generate_and_insert_sql\n result = orch._generate_and_insert_sql(job, run)\n\n # Verify executor was called\n mock_executor.execute_and_poll.assert_called_once()\n\n # Get the SQL that was submitted\n call_args = mock_executor.execute_and_poll.call_args\n submitted_sql = call_args.kwargs.get(\"sql\") or call_args[1].get(\"sql\")\n\n # Verify the SQL contains normalized dates\n assert \"'2024-09-15'\" in submitted_sql, f\"Expected '2024-09-15' in SQL:\\n{submitted_sql}\"\n assert \"'2024-09-16'\" in submitted_sql, f\"Expected '2024-09-16' in SQL:\\n{submitted_sql}\"\n\n # Verify document numbers are preserved\n assert \"'DOC-001'\" in submitted_sql\n assert \"'DOC-002'\" in submitted_sql\n\n # Verify translated text is present\n assert \"'Translated comment 1'\" in submitted_sql\n assert \"'Translated comment 2'\" in submitted_sql\n\n # Verify ClickHouse-specific quoting (backticks)\n assert \"`report_date`\" in submitted_sql\n assert \"`document_number`\" in submitted_sql\n assert \"`comment_text_en`\" in submitted_sql\n\n # Verify result\n assert result[\"status\"] == \"success\"\n\n # endregion test_generate_and_insert_sql_with_timestamps\n# #endregion TestOrchestratorInsertFlow\n" + "body": "# #region TestOrchestratorInsertFlow [TYPE Class]\n# @BRIEF Verify the orchestrator's _generate_and_insert_sql produces correct SQL for ClickHouse.\nclass TestOrchestratorInsertFlow:\n \"\"\"Verify TranslationOrchestrator._generate_and_insert_sql with mocked DB.\"\"\"\n\n # region test_generate_and_insert_sql_with_timestamps [TYPE Function]\n # @BRIEF Orchestrator builds correct rows_for_sql from source_data with timestamps.\n def test_generate_and_insert_sql_with_timestamps(self) -> None:\n \"\"\"Verify the orchestrator correctly passes source_data through to SQLGenerator.\"\"\"\n from src.plugins.translate.orchestrator import TranslationOrchestrator\n\n # Mock DB session\n db = MagicMock()\n config_manager = MagicMock()\n\n # Create mock records with timestamp source_data\n rec1 = _make_mock_record(\n target_sql=\"Translated comment 1\",\n report_date=\"1726358400000.0\",\n document_number=\"DOC-001\",\n )\n rec2 = _make_mock_record(\n target_sql=\"Translated comment 2\",\n report_date=\"1726444800000.0\",\n document_number=\"DOC-002\",\n )\n\n # Mock the DB query to return our records\n # Note: orchestrator uses .options(joinedload()) before .filter()\n mock_query = MagicMock()\n mock_query.options.return_value.filter.return_value.all.return_value = [rec1, rec2]\n db.query.return_value = mock_query\n\n # Create mock job\n job = _make_mock_job()\n\n # Create mock run\n run = MagicMock()\n run.id = \"run-test-001\"\n run.job_id = job.id\n\n # Create orchestrator\n orch = TranslationOrchestrator(db, config_manager, \"test-user\")\n\n # Mock the event_log and SupersetSqlLabExecutor\n with patch.object(orch, \"event_log\") as mock_event_log, patch(\n \"src.plugins.translate.orchestrator_sql.SupersetSqlLabExecutor\"\n ) as MockExecutor:\n mock_executor = MagicMock()\n MockExecutor.return_value = mock_executor\n mock_executor.resolve_database_id.return_value = None\n mock_executor.get_database_backend.return_value = None\n mock_executor.execute_and_poll.return_value = {\n \"status\": \"success\",\n \"query_id\": \"q-123\",\n \"rows_affected\": 2,\n }\n\n # Call _generate_and_insert_sql\n result = orch._generate_and_insert_sql(job, run)\n\n # Verify executor was called\n mock_executor.execute_and_poll.assert_called_once()\n\n # Get the SQL that was submitted\n call_args = mock_executor.execute_and_poll.call_args\n submitted_sql = call_args.kwargs.get(\"sql\") or call_args[1].get(\"sql\")\n\n # Verify the SQL contains normalized dates\n assert \"'2024-09-15'\" in submitted_sql, f\"Expected '2024-09-15' in SQL:\\n{submitted_sql}\"\n assert \"'2024-09-16'\" in submitted_sql, f\"Expected '2024-09-16' in SQL:\\n{submitted_sql}\"\n\n # Verify document numbers are preserved\n assert \"'DOC-001'\" in submitted_sql\n assert \"'DOC-002'\" in submitted_sql\n\n # Verify translated text is present\n assert \"'Translated comment 1'\" in submitted_sql\n assert \"'Translated comment 2'\" in submitted_sql\n\n # Verify ClickHouse-specific quoting (backticks)\n assert \"`report_date`\" in submitted_sql\n assert \"`document_number`\" in submitted_sql\n assert \"`comment_text_en`\" in submitted_sql\n\n # Verify result\n assert result[\"status\"] == \"success\"\n\n # endregion test_generate_and_insert_sql_with_timestamps\n# #endregion TestOrchestratorInsertFlow\n" }, { "contract_id": "TestClickHouseEndToEnd", @@ -48793,6 +49795,1192 @@ "tier_source": "AutoCalculated", "body": "# #region TestClickHouseEndToEnd [TYPE Class]\n# @BRIEF End-to-end test: generate SQL, verify structure, simulate execution.\nclass TestClickHouseEndToEnd:\n \"\"\"End-to-end verification of ClickHouse INSERT flow.\"\"\"\n\n # region test_full_insert_sql_valid_for_clickhouse [TYPE Function]\n # @BRIEF Generated SQL is valid ClickHouse INSERT with proper date formatting.\n def test_full_insert_sql_valid_for_clickhouse(self) -> None:\n \"\"\"Simulate the full flow: records -> SQL generation -> verification.\"\"\"\n # Simulate what the orchestrator does\n records = [\n _make_mock_record(\n target_sql=\"Comment about supply chain\",\n report_date=\"1726358400000.0\",\n document_number=\"INV-2024-001\",\n ),\n _make_mock_record(\n target_sql=\"Payment terms description\",\n report_date=\"1726444800000.0\",\n document_number=\"INV-2024-002\",\n ),\n _make_mock_record(\n target_sql=\"Delivery schedule notes\",\n report_date=\"1726531200000.0\",\n document_number=\"INV-2024-003\",\n ),\n ]\n\n # Build rows_for_sql (same logic as orchestrator._generate_and_insert_sql)\n job = _make_mock_job()\n columns = []\n if job.translation_column:\n columns.append(job.translation_column)\n if job.target_key_cols:\n for k in job.target_key_cols:\n if k not in columns:\n columns.append(k)\n\n rows_for_sql = []\n for rec in records:\n row_data = {}\n if job.translation_column:\n row_data[job.translation_column] = rec.target_sql or \"\"\n if job.target_key_cols:\n source_data = rec.source_data or {}\n for k in job.target_key_cols:\n row_data[k] = source_data.get(k, \"\")\n rows_for_sql.append(row_data)\n\n # Generate SQL\n sql, count = SQLGenerator.generate(\n dialect=job.database_dialect or job.target_dialect or \"clickhouse\",\n target_schema=job.target_schema,\n target_table=job.target_table,\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"INSERT\",\n )\n\n # Verify SQL structure\n assert count == 3\n assert sql.startswith(\"INSERT INTO\")\n assert \"dm_view\" in sql\n assert \"financial_comments_translated\" in sql\n\n # Verify all dates are normalized\n assert \"'2024-09-15'\" in sql # 1726358400000\n assert \"'2024-09-16'\" in sql # 1726444800000\n assert \"'2024-09-17'\" in sql # 1726531200000\n\n # Verify NO raw timestamps leaked through\n assert \"1726358400000\" not in sql\n assert \"1726444800000\" not in sql\n assert \"1726531200000\" not in sql\n\n # Verify document numbers\n assert \"'INV-2024-001'\" in sql\n assert \"'INV-2024-002'\" in sql\n assert \"'INV-2024-003'\" in sql\n\n # Verify translated texts\n assert \"'Comment about supply chain'\" in sql\n assert \"'Payment terms description'\" in sql\n assert \"'Delivery schedule notes'\" in sql\n\n # Print SQL for manual verification\n print(f\"\\n{'='*60}\")\n print(\"Generated ClickHouse INSERT SQL:\")\n print(f\"{'='*60}\")\n print(sql)\n print(f\"{'='*60}\")\n\n # region test_verification_join_query [TYPE Function]\n # @BRIEF The JOIN verification query that can be run against ClickHouse.\n def test_verification_join_query(self) -> None:\n \"\"\"Generate the verification JOIN query for manual execution.\"\"\"\n join_sql = \"\"\"\nSELECT\n f_a.report_date,\n f_a.document_number,\n comment_text AS comment_text_ru,\n counterparty,\n supply_country,\n f_c.comment_text_en\nFROM dm_view.financial_arrears f_a\nLEFT JOIN dm_view.financial_comments_translated f_c\n ON f_a.report_date::date = f_c.report_date::date\n AND f_a.document_number = f_c.document_number\nWHERE f_c.comment_text_en IS NOT NULL\nLIMIT 10\n\"\"\"\n print(f\"\\n{'='*60}\")\n print(\"Verification JOIN query (run in ClickHouse):\")\n print(f\"{'='*60}\")\n print(join_sql)\n print(f\"{'='*60}\")\n\n # Verify structure\n assert \"f_a.report_date::date = f_c.report_date::date\" in join_sql\n assert \"f_a.document_number = f_c.document_number\" in join_sql\n assert \"dm_view.financial_arrears\" in join_sql\n assert \"dm_view.financial_comments_translated\" in join_sql\n\n # endregion test_full_insert_sql_valid_for_clickhouse\n # endregion test_verification_join_query\n# #endregion TestClickHouseEndToEnd\n" }, + { + "contract_id": "TestDictionaryLegacyHub", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", + "start_line": 1, + "end_line": 20, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Legacy test module — all tests migrated to domain-specific test files:" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestDictionaryLegacyHub [C:3] [TYPE Module] [SEMANTICS test, dictionary, hub]\n# @BRIEF Legacy test module — all tests migrated to domain-specific test files:\n# test_dictionary_crud.py — DictionaryCRUD + DictionaryEntryCRUD\n# test_dictionary_import.py — Import/export operations\n# test_dictionary_filter.py — Batch filter + migration\n# test_dictionary_correction.py — Correction context capture\n# test_dictionary_prompt_builder.py — Prompt builder operations\n# test_dictionary_utils.py — Utility functions\n# @RELATION BINDS_TO -> [DictionaryManager]\n# @RATIONALE Split monolithic test module into domain-specific files per INV_7.\n# @REJECTED Keeping 1199-line test module violates module < 400 lines constraint.\n\n# All tests have been migrated to the following modules:\nfrom .test_dictionary_utils import * # noqa: F401, F403\nfrom .test_dictionary_crud import * # noqa: F401, F403\nfrom .test_dictionary_import import * # noqa: F401, F403\nfrom .test_dictionary_filter import * # noqa: F401, F403\nfrom .test_dictionary_correction import * # noqa: F401, F403\nfrom .test_dictionary_prompt_builder import * # noqa: F401, F403\n# #endregion TestDictionaryLegacyHub\n" + }, + { + "contract_id": "TestDictionaryCorrection", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_correction.py", + "start_line": 1, + "end_line": 124, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Validate InlineCorrectionService and dictionary correction flows." + }, + "relations": [ + { + "source_id": "TestDictionaryCorrection", + "relation_type": "BINDS_TO", + "target_id": "InlineCorrectionService", + "target_ref": "[InlineCorrectionService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestDictionaryCorrection [C:3] [TYPE Module] [SEMANTICS test, dictionary, correction, context]\n# @BRIEF Validate InlineCorrectionService and dictionary correction flows.\n# @RELATION BINDS_TO -> [InlineCorrectionService]\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import (\n Base,\n DictionaryEntry,\n TerminologyDictionary,\n TranslationJob,\n TranslationRun,\n TranslationBatch,\n TranslationRecord,\n TranslationLanguage,\n)\nfrom src.plugins.translate.dictionary import DictionaryManager\nfrom src.plugins.translate.service import InlineCorrectionService\n\n\n# region db_session [TYPE Fixture]\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestContextCapture:\n \"\"\"Verify context auto-capture when submitting corrections.\"\"\"\n\n # region test_context_capture_in_correction [C:2] [TYPE Function]\n def test_context_capture_in_correction(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test Dict\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Ctx Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n\n run = TranslationRun(job_id=job.id, status=\"COMPLETED\")\n db_session.add(run)\n db_session.flush()\n\n batch = TranslationBatch(run_id=run.id, batch_index=0, status=\"COMPLETED\")\n db_session.add(batch)\n db_session.flush()\n\n record = TranslationRecord(\n batch_id=batch.id, run_id=run.id, source_sql=\"hello world\",\n source_object_type=\"table_row\", source_object_name=\"Row 0\",\n source_object_id=\"0\", source_data={\"schema\": \"public\", \"table\": \"users\", \"column\": \"name\"},\n status=\"SUCCESS\",\n )\n db_session.add(record)\n db_session.flush()\n\n for lang_code in (\"ru\", \"de\", \"fr\", \"it\"):\n tl = TranslationLanguage(\n record_id=record.id, language_code=lang_code,\n source_language_detected=\"en\", translated_value=\"translated\",\n final_value=\"translated\", status=\"translated\",\n )\n db_session.add(tl)\n db_session.commit()\n\n result = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"ru\",\n dictionary_id=d.id, corrected_value=\"привет мир (исправлено)\",\n current_user=\"test_user\",\n )\n assert result[\"action\"] == \"created\"\n assert result[\"entry_id\"] is not None\n\n entry = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result[\"entry_id\"]).first()\n assert entry is not None\n assert entry.has_context is True\n assert entry.context_source == \"auto\"\n assert entry.context_data is not None\n assert \"source_data\" in entry.context_data\n assert entry.context_data[\"source_object_type\"] == \"table_row\"\n\n result2 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"de\",\n dictionary_id=d.id, corrected_value=\"another correction\",\n current_user=\"test_user\", context_data_override={\"custom_key\": \"custom_value\"},\n )\n assert result2[\"action\"] == \"created\"\n entry2 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result2[\"entry_id\"]).first()\n assert entry2.context_data == {\"custom_key\": \"custom_value\"}\n assert entry2.context_source == \"manual\"\n\n result3 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"fr\",\n dictionary_id=d.id, corrected_value=\"no context\",\n current_user=\"test_user\", keep_context=False,\n )\n assert result3[\"action\"] == \"created\"\n entry3 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result3[\"entry_id\"]).first()\n assert entry3.context_data is None\n assert entry3.has_context is False\n assert entry3.context_source == \"manual\"\n\n result4 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"it\",\n dictionary_id=d.id, corrected_value=\"with notes\",\n current_user=\"test_user\", usage_notes=\"Use this for user-facing columns only\",\n )\n assert result4[\"action\"] == \"created\"\n entry4 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result4[\"entry_id\"]).first()\n assert entry4.usage_notes == \"Use this for user-facing columns only\"\n # endregion test_context_capture_in_correction\n# #endregion TestDictionaryCorrection\n" + }, + { + "contract_id": "TestDictionaryCRUD", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_crud.py", + "start_line": 1, + "end_line": 218, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Validate DictionaryCRUD and DictionaryEntryCRUD operations.", + "TEST_EDGE": "same_term_different_lang_pair -> allowed (not duplicate)", + "TEST_INVARIANT": "unique_normalized -> verifies: [duplicate_entry, same_term_different_lang_pair allowed]" + }, + "relations": [ + { + "source_id": "TestDictionaryCRUD", + "relation_type": "BINDS_TO", + "target_id": "DictionaryCRUD", + "target_ref": "[DictionaryCRUD]" + }, + { + "source_id": "TestDictionaryCRUD", + "relation_type": "BINDS_TO", + "target_id": "DictionaryEntryCRUD", + "target_ref": "[DictionaryEntryCRUD]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "TEST_EDGE", + "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "TEST_EDGE", + "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestDictionaryCRUD [C:3] [TYPE Module] [SEMANTICS test, dictionary, crud]\n# @BRIEF Validate DictionaryCRUD and DictionaryEntryCRUD operations.\n# @RELATION BINDS_TO -> [DictionaryCRUD]\n# @RELATION BINDS_TO -> [DictionaryEntryCRUD]\n# @TEST_EDGE: duplicate_entry -> ValueError on repeated (dictionary_id, source_term_norm, source_lang, target_lang)\n# @TEST_EDGE: delete_active_job -> ValueError with active/scheduled message\n# @TEST_EDGE: same_term_different_lang_pair -> allowed (not duplicate)\n# @TEST_INVARIANT: unique_normalized -> verifies: [duplicate_entry, same_term_different_lang_pair allowed]\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import Base, DictionaryEntry, TerminologyDictionary, TranslationJob, TranslationJobDictionary\nfrom src.plugins.translate.dictionary import DictionaryManager\n\n\n# region db_session [TYPE Fixture]\n# @PURPOSE: Provide an in-memory SQLite session for each test.\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestDictionaryCRUD:\n \"\"\"Verify dictionary-level CRUD operations.\"\"\"\n\n # region test_create_dictionary [C:2] [TYPE Function]\n # @BRIEF: Verify dictionary creation and read-back.\n def test_create_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Finance Terms\",\n source_dialect=\"postgresql\", target_dialect=\"clickhouse\",\n created_by=\"test_user\", description=\"Finance-related term mappings\",\n )\n assert d.id is not None\n assert d.name == \"Finance Terms\"\n assert d.source_dialect == \"postgresql\"\n assert d.target_dialect == \"clickhouse\"\n assert d.created_by == \"test_user\"\n assert d.is_active is True\n\n fetched = DictionaryManager.get_dictionary(db_session, d.id)\n assert fetched.id == d.id\n assert fetched.name == \"Finance Terms\"\n # endregion test_create_dictionary\n\n # region test_update_dictionary [C:2] [TYPE Function]\n # @BRIEF: Verify dictionary metadata update.\n def test_update_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Old Name\", source_dialect=\"a\", target_dialect=\"b\")\n updated = DictionaryManager.update_dictionary(\n db_session, d.id, name=\"New Name\", description=\"Updated desc\", is_active=False,\n )\n assert updated.name == \"New Name\"\n assert updated.description == \"Updated desc\"\n assert updated.is_active is False\n # endregion test_update_dictionary\n\n # region test_delete_dictionary [C:2] [TYPE Function]\n # @BRIEF: Verify dictionary deletion also removes entries.\n def test_delete_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"To Delete\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n assert entry.id is not None\n DictionaryManager.delete_dictionary(db_session, d.id)\n with pytest.raises(ValueError, match=\"Dictionary not found\"):\n DictionaryManager.get_dictionary(db_session, d.id)\n # endregion test_delete_dictionary\n\n # region test_list_dictionaries [C:2] [TYPE Function]\n # @BRIEF: Verify paginated dictionary listing.\n def test_list_dictionaries(self, db_session: Session):\n for i in range(5):\n DictionaryManager.create_dictionary(db_session, name=f\"Dict {i}\", source_dialect=\"a\", target_dialect=\"b\")\n dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2)\n assert total == 5\n assert len(dicts) == 2\n # endregion test_list_dictionaries\n\n # region test_delete_dictionary_blocked_by_active_job [C:2] [TYPE Function]\n # @BRIEF: Verify deletion is blocked when attached to active/scheduled jobs.\n def test_delete_dictionary_blocked_by_active_job(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Active Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"ACTIVE\", created_by=\"test_user\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n with pytest.raises(ValueError, match=\"active/scheduled\"):\n DictionaryManager.delete_dictionary(db_session, d.id)\n # endregion test_delete_dictionary_blocked_by_active_job\n\n # region test_delete_dictionary_allowed_with_completed_job [C:2] [TYPE Function]\n # @BRIEF: Verify deletion is allowed when only completed/failed jobs reference the dictionary.\n def test_delete_dictionary_allowed_with_completed_job(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Completed Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"COMPLETED\", created_by=\"test_user\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n DictionaryManager.delete_dictionary(db_session, d.id)\n with pytest.raises(ValueError, match=\"Dictionary not found\"):\n DictionaryManager.get_dictionary(db_session, d.id)\n # endregion test_delete_dictionary_allowed_with_completed_job\n\n\nclass TestDictionaryEntryCRUD:\n \"\"\"Verify entry-level CRUD operations.\"\"\"\n\n # region test_add_entry_duplicate [C:2] [TYPE Function]\n # @BRIEF: Verify duplicate entry raises ValueError.\n def test_add_entry_duplicate(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"Hello\", \"Hola\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"Bonjour\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"HELLO\", \"Ciao\", source_language=\"en\", target_language=\"es\")\n # endregion test_add_entry_duplicate\n\n # region test_add_entry_duplicate_per_dictionary [C:2] [TYPE Function]\n # @BRIEF: Verify duplicate is per-dictionary (same term in different dicts is OK).\n def test_add_entry_duplicate_per_dictionary(self, db_session: Session):\n d1 = DictionaryManager.create_dictionary(db_session, name=\"Dict1\", source_dialect=\"a\", target_dialect=\"b\")\n d2 = DictionaryManager.create_dictionary(db_session, name=\"Dict2\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n entry = DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\", source_language=\"en\", target_language=\"es\")\n assert entry.id is not None\n # endregion test_add_entry_duplicate_per_dictionary\n\n # region test_edit_entry [C:2] [TYPE Function]\n # @BRIEF: Verify entry edit updates fields and enforces uniqueness.\n def test_edit_entry(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n updated = DictionaryManager.edit_entry(db_session, entry.id, target_term=\"HOLA!\")\n assert updated.target_term == \"HOLA!\"\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.edit_entry(db_session, entry.id, source_term=\"WORLD\")\n # endregion test_edit_entry\n\n # region test_delete_entry [C:2] [TYPE Function]\n # @BRIEF: Verify entry deletion.\n def test_delete_entry(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.delete_entry(db_session, entry.id)\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n # endregion test_delete_entry\n\n # region test_clear_entries [C:2] [TYPE Function]\n # @BRIEF: Verify clearing all entries for a dictionary.\n def test_clear_entries(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n deleted = DictionaryManager.clear_entries(db_session, d.id)\n assert deleted == 2\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n # endregion test_clear_entries\n\n # region test_add_entry_with_language_pair [C:2] [TYPE Function]\n # @BRIEF: Verify creating entry with language pair stores correctly.\n def test_add_entry_with_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Lang Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n assert entry.source_language == \"en\"\n assert entry.target_language == \"ru\"\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 1\n assert entries[0].source_language == \"en\"\n assert entries[0].target_language == \"ru\"\n # endregion test_add_entry_with_language_pair\n\n # region test_duplicate_same_language_pair [C:2] [TYPE Function]\n # @BRIEF: Verify duplicate with same language pair raises conflict.\n def test_duplicate_same_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Dup Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"ru\")\n # endregion test_duplicate_same_language_pair\n\n # region test_same_term_different_language_pair [C:2] [TYPE Function]\n # @BRIEF: Verify same term with different language pair is allowed.\n def test_same_term_different_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Multi Lang\", source_dialect=\"a\", target_dialect=\"b\")\n entry1 = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n entry2 = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"de\")\n assert entry1.id != entry2.id\n assert entry1.target_language == \"ru\"\n assert entry2.target_language == \"de\"\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 2\n # endregion test_same_term_different_language_pair\n# #endregion TestDictionaryCRUD\n" + }, + { + "contract_id": "TestDictionaryFilter", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_filter.py", + "start_line": 1, + "end_line": 228, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Validate DictionaryBatchFilter operations." + }, + "relations": [ + { + "source_id": "TestDictionaryFilter", + "relation_type": "BINDS_TO", + "target_id": "DictionaryBatchFilter", + "target_ref": "[DictionaryBatchFilter]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestDictionaryFilter [C:3] [TYPE Module] [SEMANTICS test, dictionary, filter, batch]\n# @BRIEF Validate DictionaryBatchFilter operations.\n# @RELATION BINDS_TO -> [DictionaryBatchFilter]\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import (\n Base,\n DictionaryEntry,\n TerminologyDictionary,\n TranslationJob,\n TranslationJobDictionary,\n)\nfrom src.plugins.translate.dictionary import DictionaryManager\n\n\n# region db_session [TYPE Fixture]\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestFilterBasic:\n \"\"\"Verify basic filter_for_batch behavior.\"\"\"\n\n # region test_filter_for_batch_no_dictionaries [C:2] [TYPE Function]\n def test_filter_for_batch_no_dictionaries(self, db_session: Session):\n job = TranslationJob(name=\"No Dict Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.commit()\n result = DictionaryManager.filter_for_batch(db_session, [\"hello world\"], job.id)\n assert result == []\n # endregion test_filter_for_batch_no_dictionaries\n\n # region test_filter_for_batch_matches [C:2] [TYPE Function]\n def test_filter_for_batch_matches(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test Dict\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.add_entry(db_session, d.id, \"foo\", \"bar\", source_language=\"en\", target_language=\"es\")\n\n job = TranslationJob(name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello beautiful world\", \"nothing here\", \"foo bar\"], job.id)\n assert len(result) >= 3\n\n hello_matches = [m for m in result if m[\"source_term\"] == \"hello\"]\n assert len(hello_matches) == 1\n assert hello_matches[0][\"text_index\"] == 0\n\n foo_matches = [m for m in result if m[\"source_term\"] == \"foo\"]\n assert len(foo_matches) == 1\n assert foo_matches[0][\"text_index\"] == 2\n # endregion test_filter_for_batch_matches\n\n # region test_filter_for_batch_case_insensitive [C:2] [TYPE Function]\n def test_filter_for_batch_case_insensitive(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test Dict\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"Hello World\", \"Hola Mundo\", source_language=\"en\", target_language=\"es\")\n\n job = TranslationJob(name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello world is great\"], job.id)\n assert len(result) == 1\n assert result[0][\"source_term\"] == \"Hello World\"\n assert result[0][\"target_term\"] == \"Hola Mundo\"\n # endregion test_filter_for_batch_case_insensitive\n\n # region test_filter_for_batch_word_boundary [C:2] [TYPE Function]\n def test_filter_for_batch_word_boundary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test Dict\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"cat\", \"gato\", source_language=\"en\", target_language=\"es\")\n\n job = TranslationJob(name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"catalog\"], job.id)\n assert len(result) == 0\n\n result = DictionaryManager.filter_for_batch(db_session, [\"the cat sat\"], job.id)\n assert len(result) == 1\n # endregion test_filter_for_batch_word_boundary\n\n\nclass TestFilterPriority:\n \"\"\"Verify dictionary link order priority in filtering.\"\"\"\n\n # region test_filter_for_batch_multi_dictionary_priority [C:2] [TYPE Function]\n def test_filter_for_batch_multi_dictionary_priority(self, db_session: Session):\n d1 = DictionaryManager.create_dictionary(db_session, name=\"Priority1\", source_dialect=\"a\", target_dialect=\"b\")\n d2 = DictionaryManager.create_dictionary(db_session, name=\"Priority2\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\", source_language=\"en\", target_language=\"es\")\n\n job = TranslationJob(name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n link1 = TranslationJobDictionary(job_id=job.id, dictionary_id=d1.id)\n link2 = TranslationJobDictionary(job_id=job.id, dictionary_id=d2.id)\n db_session.add(link1)\n db_session.add(link2)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello\"], job.id)\n assert len(result) == 2\n assert result[0][\"dictionary_id\"] == d1.id\n assert result[0][\"target_term\"] == \"hola\"\n assert result[1][\"dictionary_id\"] == d2.id\n assert result[1][\"target_term\"] == \"bonjour\"\n # endregion test_filter_for_batch_multi_dictionary_priority\n\n\nclass TestFilterLanguagePair:\n \"\"\"Verify language-pair filtering in filter_for_batch.\"\"\"\n\n # region test_filter_for_batch_with_language_pair [C:2] [TYPE Function]\n def test_filter_for_batch_with_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Lang Filter\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"мир\", source_language=\"en\", target_language=\"ru\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"de\")\n\n job = TranslationJob(name=\"Lang Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello world\"], job.id, source_language=\"en\", target_language=\"ru\")\n assert len(result) == 2\n assert all(m[\"target_language\"] == \"ru\" for m in result)\n\n result2 = DictionaryManager.filter_for_batch(db_session, [\"hello\"], job.id, source_language=\"en\", target_language=\"de\")\n assert len(result2) == 1\n assert result2[0][\"target_language\"] == \"de\"\n assert result2[0][\"target_term\"] == \"hallo\"\n # endregion test_filter_for_batch_with_language_pair\n\n # region test_filter_for_batch_target_language_only [C:2] [TYPE Function]\n def test_filter_for_batch_target_language_only(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Target Only\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"de\")\n DictionaryManager.add_entry(db_session, d.id, \"bonjour\", \"hallo\", source_language=\"fr\", target_language=\"de\")\n\n job = TranslationJob(name=\"Tgt Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello bonjour\"], job.id, target_language=\"de\")\n assert len(result) == 2\n assert all(m[\"target_language\"] == \"de\" for m in result)\n\n result2 = DictionaryManager.filter_for_batch(db_session, [\"hello\"], job.id, target_language=\"ru\")\n assert len(result2) == 1\n assert result2[0][\"target_language\"] == \"ru\"\n # endregion test_filter_for_batch_target_language_only\n\n\nclass TestMigration:\n \"\"\"Verify migration of old-style entries.\"\"\"\n\n # region test_migrate_old_entries [C:2] [TYPE Function]\n def test_migrate_old_entries(self, db_session: Session):\n d = TerminologyDictionary(name=\"Old Dict\", source_dialect=\"a\", target_dialect=\"b\")\n db_session.add(d)\n db_session.flush()\n\n entry1 = DictionaryEntry(\n dictionary_id=d.id, source_term=\"hello\", source_term_normalized=\"hello\",\n target_term=\"привет\", source_language=\"und\", target_language=\"und\",\n )\n entry2 = DictionaryEntry(\n dictionary_id=d.id, source_term=\"world\", source_term_normalized=\"world\",\n target_term=\"мир\", source_language=\"und\", target_language=\"und\",\n origin_source_language=\"en\",\n )\n db_session.add(entry1)\n db_session.add(entry2)\n db_session.commit()\n\n result = DictionaryManager.migrate_old_entries(db_session)\n assert result[\"total_processed\"] >= 2\n assert result[\"migrated_source\"] >= 1\n assert result[\"migrated_target\"] == 0\n\n db_session.refresh(entry1)\n assert entry1.target_language == \"und\"\n assert entry1.source_language == \"und\"\n\n db_session.refresh(entry2)\n assert entry2.source_language == \"en\"\n assert entry2.target_language == \"und\"\n # endregion test_migrate_old_entries\n# #endregion TestDictionaryFilter\n" + }, + { + "contract_id": "TestDictionaryImport", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_import.py", + "start_line": 1, + "end_line": 177, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Validate DictionaryImportExport operations.", + "TEST_EDGE": "import_invalid_format -> ValueError for missing required columns" + }, + "relations": [ + { + "source_id": "TestDictionaryImport", + "relation_type": "BINDS_TO", + "target_id": "DictionaryImportExport", + "target_ref": "[DictionaryImportExport]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "TEST_EDGE", + "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "TEST_EDGE", + "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestDictionaryImport [C:3] [TYPE Module] [SEMANTICS test, dictionary, import, export]\n# @BRIEF Validate DictionaryImportExport operations.\n# @RELATION BINDS_TO -> [DictionaryImportExport]\n# @TEST_EDGE: import_invalid_format -> ValueError for missing required columns\n\nimport csv\nimport io\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import Base\nfrom src.plugins.translate.dictionary import DictionaryManager\n\n\n# region db_session [TYPE Fixture]\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestImportOverwrite:\n \"\"\"Verify CSV import with overwrite conflict mode.\"\"\"\n\n # region test_import_csv_overwrite [C:2] [TYPE Function]\n def test_import_csv_overwrite(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n\n csv_content = \"source_term,target_term,context_notes,source_language,target_language\\nhello,HELLO,,en,es\\nworld,mundo,,en,es\"\n result = DictionaryManager.import_entries(db_session, d.id, csv_content, delimiter=\",\", on_conflict=\"overwrite\")\n assert result[\"total\"] == 2\n assert result[\"created\"] == 1\n assert result[\"updated\"] == 1\n assert result[\"skipped\"] == 0\n\n entries, _ = DictionaryManager.list_entries(db_session, d.id)\n hello_entry = [e for e in entries if e.source_term_normalized == \"hello\"][0]\n assert hello_entry.target_term == \"HELLO\"\n # endregion test_import_csv_overwrite\n\n\nclass TestImportKeepExisting:\n \"\"\"Verify CSV import with keep_existing conflict mode.\"\"\"\n\n # region test_import_csv_keep_existing [C:2] [TYPE Function]\n def test_import_csv_keep_existing(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n\n csv_content = \"source_term,target_term,source_language,target_language\\nhello,HELLO,en,es\\nworld,mundo,en,es\"\n result = DictionaryManager.import_entries(db_session, d.id, csv_content, delimiter=\",\", on_conflict=\"keep_existing\")\n assert result[\"created\"] == 1\n assert result[\"updated\"] == 0\n assert result[\"skipped\"] == 1\n\n entries, _ = DictionaryManager.list_entries(db_session, d.id)\n hello_entry = [e for e in entries if e.source_term_normalized == \"hello\"][0]\n assert hello_entry.target_term == \"hola\"\n # endregion test_import_csv_keep_existing\n\n\nclass TestImportCancel:\n \"\"\"Verify CSV import with cancel conflict mode.\"\"\"\n\n # region test_import_csv_cancel_on_conflict [C:2] [TYPE Function]\n def test_import_csv_cancel_on_conflict(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n\n csv_content = \"source_term,target_term,source_language,target_language\\nhello,HELLO,en,es\\nworld,mundo,en,es\"\n result = DictionaryManager.import_entries(db_session, d.id, csv_content, delimiter=\",\", on_conflict=\"cancel\")\n assert result[\"total\"] == 2\n assert result[\"created\"] == 1\n assert result[\"updated\"] == 0\n assert len(result[\"errors\"]) == 1\n assert \"Conflict\" in result[\"errors\"][0][\"error\"]\n # endregion test_import_csv_cancel_on_conflict\n\n\nclass TestImportFormat:\n \"\"\"Verify import format handling.\"\"\"\n\n # region test_import_tsv [C:2] [TYPE Function]\n def test_import_tsv(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n tsv_content = \"source_term\\ttarget_term\\tsource_language\\ttarget_language\\nhello\\thola\\ten\\tes\\nworld\\tmundo\\ten\\tes\"\n result = DictionaryManager.import_entries(db_session, d.id, tsv_content, delimiter=\"\\t\", on_conflict=\"overwrite\")\n assert result[\"created\"] == 2\n assert result[\"total\"] == 2\n # endregion test_import_tsv\n\n # region test_import_invalid_format [C:2] [TYPE Function]\n def test_import_invalid_format(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n with pytest.raises(ValueError, match=\"source_term\"):\n DictionaryManager.import_entries(db_session, d.id, \"name,value\\nhello,hola\", delimiter=\",\", on_conflict=\"overwrite\")\n # endregion test_import_invalid_format\n\n # region test_import_empty_rows [C:2] [TYPE Function]\n def test_import_empty_rows(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n csv_content = \"source_term,target_term\\nhello,hola\\n,world\\nfoo,\"\n result = DictionaryManager.import_entries(db_session, d.id, csv_content, delimiter=\",\", on_conflict=\"overwrite\")\n assert result[\"created\"] == 1\n assert len(result[\"errors\"]) == 2\n # endregion test_import_empty_rows\n\n # region test_import_preview [C:2] [TYPE Function]\n def test_import_preview(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n\n csv_content = \"source_term,target_term,source_language,target_language\\nhello,HELLO,en,es\\nworld,mundo,en,es\"\n result = DictionaryManager.import_entries(db_session, d.id, csv_content, delimiter=\",\", on_conflict=\"overwrite\", preview_only=True)\n assert len(result[\"preview\"]) == 2\n assert result[\"preview\"][0][\"is_conflict\"] is True\n assert result[\"preview\"][0][\"existing_target_term\"] == \"hola\"\n assert result[\"preview\"][1][\"is_conflict\"] is False\n\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 1\n # endregion test_import_preview\n\n\nclass TestExportEntries:\n \"\"\"Verify export functionality.\"\"\"\n\n # region test_export_entries [C:2] [TYPE Function]\n def test_export_entries(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Export Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"мир\", source_language=\"en\", target_language=\"ru\")\n\n csv_output = DictionaryManager.export_entries(db_session, d.id)\n assert \"source_language\" in csv_output\n assert \"target_language\" in csv_output\n assert \"en\" in csv_output\n assert \"ru\" in csv_output\n\n reader = csv.DictReader(io.StringIO(csv_output))\n rows = list(reader)\n assert len(rows) == 2\n assert rows[0][\"source_language\"] == \"en\"\n assert rows[0][\"target_language\"] == \"ru\"\n # endregion test_export_entries\n\n # region test_import_with_default_language [C:2] [TYPE Function]\n def test_import_with_default_language(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Default Lang Import\", source_dialect=\"a\", target_dialect=\"b\")\n csv_content = \"source_term,target_term\\nhello,привет\\nworld,мир\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content, delimiter=\",\", on_conflict=\"overwrite\",\n default_source_language=\"en\", default_target_language=\"ru\",\n )\n assert result[\"created\"] == 2\n\n entries, _ = DictionaryManager.list_entries(db_session, d.id)\n for e in entries:\n assert e.source_language == \"en\"\n assert e.target_language == \"ru\"\n # endregion test_import_with_default_language\n# #endregion TestDictionaryImport\n" + }, + { + "contract_id": "TestDictionaryPromptBuilder", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_prompt_builder.py", + "start_line": 1, + "end_line": 111, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Validate ContextAwarePromptBuilder operations: Jaccard similarity, context truncation, entry rendering." + }, + "relations": [ + { + "source_id": "TestDictionaryPromptBuilder", + "relation_type": "BINDS_TO", + "target_id": "ContextAwarePromptBuilder", + "target_ref": "[ContextAwarePromptBuilder]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestDictionaryPromptBuilder [C:3] [TYPE Module] [SEMANTICS test, dictionary, prompt, context, similarity]\n# @BRIEF Validate ContextAwarePromptBuilder operations: Jaccard similarity, context truncation, entry rendering.\n# @RELATION BINDS_TO -> [ContextAwarePromptBuilder]\n\nfrom src.plugins.translate.prompt_builder import ContextAwarePromptBuilder\n\n\nclass TestJaccardSimilarity:\n \"\"\"Verify Jaccard similarity computation.\"\"\"\n\n # region test_jaccard_similarity [C:2] [TYPE Function]\n def test_jaccard_similarity(self):\n builder = ContextAwarePromptBuilder()\n\n ctx1 = {\"schema\": \"public\", \"table\": \"users\"}\n ctx2 = {\"schema\": \"public\", \"table\": \"users\"}\n sim = builder.compute_context_similarity(ctx1, ctx2)\n assert sim == 1.0\n\n ctx3 = {\"schema\": \"private\"}\n sim = builder.compute_context_similarity(ctx1, ctx3)\n assert sim == 0.0\n\n ctx4 = {\"schema\": \"public\", \"table\": \"orders\"}\n sim = builder.compute_context_similarity(ctx1, ctx4)\n assert 0.33 <= sim <= 0.34\n\n assert builder.compute_context_similarity(None, ctx1) == 0.0\n assert builder.compute_context_similarity(ctx1, None) == 0.0\n assert builder.compute_context_similarity({}, ctx1) == 0.0\n\n ctx5 = {\"schema\": \"PUBLIC\", \"table\": \"USERS\"}\n sim = builder.compute_context_similarity(ctx1, ctx5)\n assert sim == 1.0\n # endregion test_jaccard_similarity\n\n\nclass TestContextTruncation:\n \"\"\"Verify context string truncation.\"\"\"\n\n # region test_context_truncation [C:2] [TYPE Function]\n def test_context_truncation(self):\n builder = ContextAwarePromptBuilder()\n\n long_val = \"x\" * 500\n long_context = {f\"key{i}\": long_val for i in range(10)}\n entry = {\"source_term\": \"hello\", \"target_term\": \"world\", \"has_context\": True, \"context_data\": long_context, \"usage_notes\": None}\n rendered = builder.render_entry(entry, priority=False)\n assert len(rendered) < 2200\n assert \"...[truncated]\" in rendered\n\n short_entry = {\"source_term\": \"hello\", \"target_term\": \"world\", \"has_context\": True, \"context_data\": {\"table\": \"users\"}, \"usage_notes\": None}\n short_rendered = builder.render_entry(short_entry, priority=False)\n assert \"...[truncated]\" not in short_rendered\n assert \"(context: table=users)\" in short_rendered\n\n priority_rendered = builder.render_entry(short_entry, priority=True)\n assert priority_rendered.startswith(\"# PRIORITY (context match)\")\n\n notes_entry = {\"source_term\": \"hello\", \"target_term\": \"world\", \"has_context\": False, \"context_data\": None, \"usage_notes\": \"Use for admin panels\"}\n notes_rendered = builder.render_entry(notes_entry, priority=False)\n assert \"# Usage: Use for admin panels\" in notes_rendered\n # endregion test_context_truncation\n\n\nclass TestRenderEntry:\n \"\"\"Verify render_entry handles both dicts and objects.\"\"\"\n\n # region test_render_entry_dict_and_object [C:2] [TYPE Function]\n def test_render_entry_dict_and_object(self):\n builder = ContextAwarePromptBuilder()\n\n dict_entry = {\"source_term\": \"hello\", \"target_term\": \"hola\", \"has_context\": False, \"context_data\": None, \"usage_notes\": None}\n dict_result = builder.render_entry(dict_entry)\n\n class FakeEntry:\n source_term = \"hello\"\n target_term = \"hola\"\n has_context = False\n context_data = None\n usage_notes = None\n\n obj_result = builder.render_entry(FakeEntry())\n assert dict_result == obj_result\n assert dict_result == '\"hello\" -> \"hola\"'\n # endregion test_render_entry_dict_and_object\n\n\nclass TestPrioritization:\n \"\"\"Verify build_context_entries sorts priority entries first.\"\"\"\n\n # region test_build_context_entries_prioritization [C:2] [TYPE Function]\n def test_build_context_entries_prioritization(self):\n builder = ContextAwarePromptBuilder()\n\n entries = [\n {\"source_term\": \"high_match\", \"target_term\": \"alta_coincidencia\", \"has_context\": True, \"context_data\": {\"schema\": \"public\", \"table\": \"users\"}, \"usage_notes\": None},\n {\"source_term\": \"no_match\", \"target_term\": \"sin_coincidencia\", \"has_context\": True, \"context_data\": {\"schema\": \"private\", \"table\": \"config\"}, \"usage_notes\": None},\n {\"source_term\": \"no_context\", \"target_term\": \"sin_contexto\", \"has_context\": False, \"context_data\": None, \"usage_notes\": None},\n ]\n\n row_context = {\"schema\": \"public\", \"table\": \"users\"}\n rendered = builder.build_context_entries(entries, row_context)\n\n assert len(rendered) == 3\n assert rendered[0].startswith(\"# PRIORITY (context match)\")\n assert '\"high_match\"' in rendered[0]\n assert not rendered[1].startswith(\"# PRIORITY\")\n assert not rendered[2].startswith(\"# PRIORITY\")\n # endregion test_build_context_entries_prioritization\n# #endregion TestDictionaryPromptBuilder\n" + }, + { + "contract_id": "TestDictionaryUtils", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_utils.py", + "start_line": 1, + "end_line": 36, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Validate utility functions: _normalize_term and _detect_delimiter." + }, + "relations": [ + { + "source_id": "TestDictionaryUtils", + "relation_type": "BINDS_TO", + "target_id": "_utils", + "target_ref": "[_utils]" + } + ], + "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 -> [_utils]\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": "TestTokenBudget", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/__tests__/test_token_budget.py", + "start_line": 1, + "end_line": 195, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Verify estimate_token_budget contracts — safe batch sizing, auto-reduction, warning generation.", + "TEST_EDGE": "conservative_min — even with huge rows, batch_size_adjusted >= 1", + "TEST_INVARIANT": "warning is None when batch fits, str when reduced" + }, + "relations": [ + { + "source_id": "TestTokenBudget", + "relation_type": "BINDS_TO", + "target_id": "estimate_token_budget:Module", + "target_ref": "[estimate_token_budget:Module]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "TEST_EDGE", + "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Function", + "Block" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "TEST_EDGE", + "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestTokenBudget [C:3] [TYPE Module] [SEMANTICS test, token, budget, estimation, batch, translate]\n# @BRIEF Verify estimate_token_budget contracts — safe batch sizing, auto-reduction, warning generation.\n# @RELATION BINDS_TO -> [estimate_token_budget:Module]\n# @TEST_EDGE: empty_rows — empty source rows returns batch_size_adjusted=1\n# @TEST_EDGE: small_rows — short text fits in a single batch at requested size\n# @TEST_EDGE: large_rows — long text causes batch size reduction\n# @TEST_EDGE: multi_language — more target languages increases output estimate\n# @TEST_EDGE: context_columns — context columns increase input token estimate\n# @TEST_EDGE: dictionary_entries — glossary entries increase input token estimate\n# @TEST_EDGE: auto_calc — no batch_size specified, auto-calculates max safe\n# @TEST_EDGE: exact_fit — exactly fits context window, batch_adjusted == requested\n# @TEST_EDGE: conservative_min — even with huge rows, batch_size_adjusted >= 1\n# @TEST_INVARIANT: batch_size_adjusted >= 1 always\n# @TEST_INVARIANT: max_output_needed between MIN_MAX_TOKENS(4096) and max_output_tokens(8192)\n# @TEST_INVARIANT: warning is None when batch fits, str when reduced\n\nfrom src.plugins.translate._token_budget import DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_OUTPUT_TOKENS, estimate_token_budget\n\n\n# region _make_row [TYPE Function]\n# @BRIEF Create a test source row dict.\ndef _make_row(text: str, **context) -> dict:\n row = {\"source_text\": text, \"row_index\": \"0\"}\n row.update(context)\n return row\n# endregion _make_row\n\n\n# region TestTokenBudget [TYPE Class]\n# @BRIEF Test suite for estimate_token_budget.\nclass TestTokenBudget:\n\n # region test_small_rows_fit_at_requested_size [TYPE Function]\n # @BRIEF Short text rows fill the requested batch_size without reduction.\n def test_small_rows_fit_at_requested_size(self):\n \"\"\"Short text of ~50 chars should fit 50-row batch easily.\"\"\"\n rows = [_make_row(\"Hello world, this is a short text for translation.\")] * 50\n result = estimate_token_budget(\n source_rows=rows,\n target_languages=[\"ru\"],\n batch_size=50,\n )\n assert result[\"batch_size_adjusted\"] == 50\n assert result[\"estimated_input_tokens\"] > 0\n assert result[\"estimated_output_tokens\"] > 0\n assert result[\"max_output_needed\"] >= 4096\n assert result[\"max_output_needed\"] <= DEFAULT_MAX_OUTPUT_TOKENS\n assert result[\"warning\"] is None\n # endregion test_small_rows_fit_at_requested_size\n\n # region test_large_rows_reduce_batch_size [TYPE Function]\n # @BRIEF Very long text rows force batch size reduction to fit context window.\n def test_large_rows_reduce_batch_size(self):\n \"\"\"~10000 char rows should cause batch reduction from 50 to a smaller number.\"\"\"\n long_text = \"X\" * 10000 # ~4545 tokens each at 2.2 chars/token\n rows = [_make_row(long_text)] * 50\n result = estimate_token_budget(\n source_rows=rows,\n target_languages=[\"ru\", \"en\"],\n batch_size=50,\n )\n # Each row is ~4545 tokens input, plus ~2400 output (2 langs * 60 * 20 + 2000)\n # With 50 rows: ~227K tokens — way over 64K. Should reduce significantly.\n assert result[\"batch_size_adjusted\"] < 50\n assert result[\"batch_size_adjusted\"] >= 1\n assert result[\"warning\"] is not None\n assert \"Reduced batch size\" in result[\"warning\"]\n # endregion test_large_rows_reduce_batch_size\n\n # region test_multi_language_increases_output_estimate [TYPE Function]\n # @BRIEF More target languages increase the estimated output tokens.\n def test_multi_language_increases_output_estimate(self):\n \"\"\"4 target languages should have higher output estimate than 1.\"\"\"\n rows = [_make_row(\"Short text.\")] * 10\n\n single_lang = estimate_token_budget(rows, [\"ru\"], batch_size=10)\n multi_lang = estimate_token_budget(rows, [\"ru\", \"en\", \"fr\", \"de\"], batch_size=10)\n\n assert multi_lang[\"estimated_output_tokens\"] > single_lang[\"estimated_output_tokens\"]\n # Both should fit without reduction\n assert single_lang[\"batch_size_adjusted\"] == 10\n assert multi_lang[\"batch_size_adjusted\"] == 10\n # endregion test_multi_language_increases_output_estimate\n\n # region test_context_columns_increase_input [TYPE Function]\n # @BRIEF Context columns add to the input token estimate.\n def test_context_columns_increase_input(self):\n \"\"\"Rows with context columns should have higher input tokens.\"\"\"\n rows = [_make_row(\"Short text.\")] * 10\n\n no_context = estimate_token_budget(rows, [\"ru\"], batch_size=10)\n with_context = estimate_token_budget(\n rows, [\"ru\"], batch_size=10,\n context_columns=[\"description\", \"category\"],\n )\n\n assert with_context[\"estimated_input_tokens\"] > no_context[\"estimated_input_tokens\"]\n # endregion test_context_columns_increase_input\n\n # region test_dictionary_entries_increase_input [TYPE Function]\n # @BRIEF Dictionary entries add to the input token estimate.\n def test_dictionary_entries_increase_input(self):\n \"\"\"Dictionary entries should increase estimated_input_tokens.\"\"\"\n rows = [_make_row(\"Short text.\")] * 10\n dict_entries = [{\"source_term\": f\"term_{i}\", \"target_term\": f\"trans_{i}\"} for i in range(100)]\n\n no_dict = estimate_token_budget(rows, [\"ru\"], batch_size=10)\n with_dict = estimate_token_budget(rows, [\"ru\"], batch_size=10, dictionary_entries=dict_entries)\n\n assert with_dict[\"estimated_input_tokens\"] > no_dict[\"estimated_input_tokens\"]\n # endregion test_dictionary_entries_increase_input\n\n # region test_auto_calc_finds_safe_size [TYPE Function]\n # @BRIEF Without batch_size specified, auto-calculate max safe batch.\n def test_auto_calc_finds_safe_size(self):\n \"\"\"Auto-calculation should find the max rows that fit context window.\"\"\"\n rows = [_make_row(\"A\" * 500)] * 100 # ~227 tokens/row\n\n result = estimate_token_budget(\n source_rows=rows,\n target_languages=[\"ru\", \"en\"],\n batch_size=None, # auto-calc\n )\n\n assert result[\"batch_size_adjusted\"] >= 1\n assert result[\"batch_size_adjusted\"] <= 100\n # Verify it found a reasonable size\n assert result[\"estimated_input_tokens\"] + result[\"max_output_needed\"] <= DEFAULT_CONTEXT_WINDOW\n # endregion test_auto_calc_finds_safe_size\n\n # region test_empty_rows_returns_minimum [TYPE Function]\n # @BRIEF Empty source rows returns batch_size_adjusted=1 and minimum estimates.\n def test_empty_rows_returns_minimum(self):\n \"\"\"Empty rows list should return batch_size_adjusted=1 with min estimates.\"\"\"\n result = estimate_token_budget(\n source_rows=[],\n target_languages=[\"ru\"],\n batch_size=10,\n )\n assert result[\"batch_size_adjusted\"] == 1\n assert result[\"estimated_input_tokens\"] >= 300 # at least prompt base\n assert result[\"estimated_output_tokens\"] >= 2000 # at least reasoning overhead\n # endregion test_empty_rows_returns_minimum\n\n # region test_exact_fit_no_warning [TYPE Function]\n # @BRIEF When batch fits exactly, no warning is generated.\n def test_exact_fit_no_warning(self):\n \"\"\"Small batch that fits easily should have no warning.\"\"\"\n rows = [_make_row(\"Short text.\")] * 5\n result = estimate_token_budget(rows, [\"ru\"], batch_size=5)\n assert result[\"warning\"] is None\n assert result[\"batch_size_adjusted\"] == 5\n # endregion test_exact_fit_no_warning\n\n # region test_huge_rows_still_return_min_one [TYPE Function]\n # @BRIEF Even with massive rows, batch_size_adjusted is at least 1.\n def test_huge_rows_still_return_min_one(self):\n \"\"\"Massive rows that exceed context window still return batch_size_adjusted >= 1.\"\"\"\n huge_text = \"X\" * 500000 # ~227K tokens — exceeds context window\n rows = [_make_row(huge_text)] * 10\n result = estimate_token_budget(rows, [\"ru\", \"en\", \"fr\"], batch_size=10)\n assert result[\"batch_size_adjusted\"] >= 1\n # endregion test_huge_rows_still_return_min_one\n\n # region test_max_output_needed_bounds [TYPE Function]\n # @BRIEF max_output_needed stays within reasonable bounds.\n def test_max_output_needed_bounds(self):\n \"\"\"max_output_needed is between MIN_MAX_TOKENS and max_output_tokens.\"\"\"\n rows = [_make_row(\"Short text.\")] * 5\n result = estimate_token_budget(rows, [\"ru\", \"en\"], batch_size=5)\n assert 4096 <= result[\"max_output_needed\"] <= DEFAULT_MAX_OUTPUT_TOKENS\n # endregion test_max_output_needed_bounds\n\n # region test_single_row_never_reduced [TYPE Function]\n # @BRIEF Batch of 1 should never be reduced (batch_size_adjusted >= requested when requested=1).\n def test_single_row_never_reduced(self):\n \"\"\"Even a very long single row should still fit (batch_size_adjusted=1).\"\"\"\n long_text = \"X\" * 100000 # large but still fits context if alone\n rows = [_make_row(long_text)]\n result = estimate_token_budget(rows, [\"ru\", \"en\", \"fr\", \"de\"], batch_size=1)\n assert result[\"batch_size_adjusted\"] == 1\n # endregion test_single_row_never_reduced\n\n # region test_no_target_languages_defaults_to_en [TYPE Function]\n # @BRIEF When target_languages is empty, defaults to [\"en\"].\n def test_no_target_languages_defaults_to_en(self):\n \"\"\"Empty target_languages defaults to ['en'] without error.\"\"\"\n rows = [_make_row(\"Short text.\")] * 5\n result = estimate_token_budget(rows, [], batch_size=5)\n assert result[\"batch_size_adjusted\"] == 5\n assert result[\"estimated_output_tokens\"] > 0\n # endregion test_no_target_languages_defaults_to_en\n\n# endregion TestTokenBudget\n# #endregion TestTokenBudget\n" + }, + { + "contract_id": "BatchInsertService", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 1, + "end_line": 233, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Insert successful translation records into target table via Superset SQL Lab." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region BatchInsertService [C:3] [TYPE Module] [SEMANTICS translate, insert, sqllab, upsert, target]\n# @BRIEF Insert successful translation records into target table via Superset SQL Lab.\n# Builds INSERT SQL (original + per-language rows), resolves backend dialect,\n# and executes via SupersetSqlLabExecutor.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [SQLGenerator], [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Batch has committed TranslationRecords with status SUCCESS. Job has target_table configured.\n# @POST Per record, N+1 rows are INSERTED: 1 original + N translations.\n# @SIDE_EFFECT HTTP call to Superset SQL Lab API. Writes to target database.\n# @RATIONALE Extracted from BatchProcessingService (583 lines) to comply with INV_7.\n# @REJECTED Inline insert logic in process_batch — made the function 583 lines.\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRecord\nfrom .sql_generator import SQLGenerator, _normalize_timestamp_value\nfrom .superset_executor import SupersetSqlLabExecutor\n\n\n# #region insert_batch_to_target [C:3] [TYPE Function] [SEMANTICS translate, insert, orchestrate]\n# @BRIEF Insert successful records from a single batch into the target table.\ndef insert_batch_to_target(\n db: Session, config_manager: ConfigManager,\n job: TranslationJob, batch_id: str, run_id: str,\n) -> None:\n \"\"\"Insert successful batch records into the target table via Superset SQL Lab.\"\"\"\n with belief_scope(\"BatchInsertService.insert_batch_to_target\"):\n records = _fetch_batch_records(db, batch_id)\n if not records:\n return\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n columns = _build_target_columns(job, effective_target)\n context_keys = _build_context_keys(job, effective_target)\n rows_for_sql = _build_insert_rows(records, job, effective_target, primary_language, context_keys)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n dialect, executor = _resolve_insert_backend(config_manager, job, batch_id)\n if dialect is None:\n return\n\n sql, row_count = _generate_insert_sql(dialect, job, columns, rows_for_sql, batch_id)\n if sql is None:\n return\n\n _execute_insert_sql(executor, sql, batch_id, row_count)\n# #endregion insert_batch_to_target\n\n\n# #region _fetch_batch_records [C:1] [TYPE Function]\ndef _fetch_batch_records(db: Session, batch_id: str) -> list[TranslationRecord]:\n \"\"\"Fetch successful TranslationRecords for a batch with languages.\"\"\"\n return (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.batch_id == batch_id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n# #endregion _fetch_batch_records\n\n\n# #region _build_target_columns [C:1] [TYPE Function]\ndef _build_target_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build the list of column names for the target INSERT.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion _build_target_columns\n\n\n# #region _build_context_keys [C:1] [TYPE Function]\ndef _build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context keys for JSON bundled column.\"\"\"\n context_keys = list(job.context_columns or [])\n if (\n job.translation_column\n and job.translation_column != effective_target\n and job.translation_column not in context_keys\n ):\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion _build_context_keys\n\n\n# #region _build_insert_rows [C:3] [TYPE Function]\ndef _build_insert_rows(\n records: list[TranslationRecord], job: TranslationJob,\n effective_target: str | None, primary_language: str, context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row dicts for the INSERT SQL statement.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data: dict[str, str] = {}\n for key in context_keys:\n val = source_data.get(key)\n context_data[key] = str(val) if val is not None else \"\"\n\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback = dict(base_row)\n if effective_target:\n fallback[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback[job.target_language_column] = primary_language\n fallback[\"is_original\"] = 0\n rows_for_sql.append(fallback)\n return rows_for_sql\n# #endregion _build_insert_rows\n\n\n# #region _resolve_insert_backend [C:1] [TYPE Function]\ndef _resolve_insert_backend(\n config_manager: ConfigManager, job: TranslationJob, batch_id: str,\n) -> tuple[str | None, SupersetSqlLabExecutor | None]:\n \"\"\"Resolve the database backend and SQL executor for batch insert.\"\"\"\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend for batch insert\",\n {\"batch_id\": batch_id, \"error\": str(e)})\n return None, None\n dialect = real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n return dialect, executor\n# #endregion _resolve_insert_backend\n\n\n# #region _generate_insert_sql [C:1] [TYPE Function]\ndef _generate_insert_sql(\n dialect: str, job: TranslationJob, columns: list[str],\n rows_for_sql: list[dict[str, object]], batch_id: str = \"\",\n) -> tuple[str | None, int]:\n \"\"\"Generate INSERT SQL using SQLGenerator.\"\"\"\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect, target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns, rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n return sql, row_count\n except ValueError as e:\n logger.explore(\"SQL generation failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return None, 0\n# #endregion _generate_insert_sql\n\n\n# #region _execute_insert_sql [C:1] [TYPE Function]\ndef _execute_insert_sql(executor: SupersetSqlLabExecutor, sql: str, batch_id: str, row_count: int) -> None:\n \"\"\"Execute the INSERT SQL via Superset SQL Lab.\"\"\"\n try:\n result = executor.execute_and_poll(sql=sql, max_polls=30, poll_interval_seconds=2.0)\n except Exception as e:\n logger.explore(\"Superset SQL submission failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return\n logger.reason(f\"Batch {batch_id[:12]} inserted {row_count} rows\",\n {\"batch_id\": batch_id, \"rows\": row_count, \"status\": result.get(\"status\")})\n# #endregion _execute_insert_sql\n# #endregion BatchInsertService\n" + }, + { + "contract_id": "insert_batch_to_target", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 27, + "end_line": 58, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Insert successful records from a single batch into the target table." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region insert_batch_to_target [C:3] [TYPE Function] [SEMANTICS translate, insert, orchestrate]\n# @BRIEF Insert successful records from a single batch into the target table.\ndef insert_batch_to_target(\n db: Session, config_manager: ConfigManager,\n job: TranslationJob, batch_id: str, run_id: str,\n) -> None:\n \"\"\"Insert successful batch records into the target table via Superset SQL Lab.\"\"\"\n with belief_scope(\"BatchInsertService.insert_batch_to_target\"):\n records = _fetch_batch_records(db, batch_id)\n if not records:\n return\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n columns = _build_target_columns(job, effective_target)\n context_keys = _build_context_keys(job, effective_target)\n rows_for_sql = _build_insert_rows(records, job, effective_target, primary_language, context_keys)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n dialect, executor = _resolve_insert_backend(config_manager, job, batch_id)\n if dialect is None:\n return\n\n sql, row_count = _generate_insert_sql(dialect, job, columns, rows_for_sql, batch_id)\n if sql is None:\n return\n\n _execute_insert_sql(executor, sql, batch_id, row_count)\n# #endregion insert_batch_to_target\n" + }, + { + "contract_id": "_fetch_batch_records", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 61, + "end_line": 74, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _fetch_batch_records [C:1] [TYPE Function]\ndef _fetch_batch_records(db: Session, batch_id: str) -> list[TranslationRecord]:\n \"\"\"Fetch successful TranslationRecords for a batch with languages.\"\"\"\n return (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.batch_id == batch_id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n# #endregion _fetch_batch_records\n" + }, + { + "contract_id": "_build_target_columns", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 77, + "end_line": 100, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _build_target_columns [C:1] [TYPE Function]\ndef _build_target_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build the list of column names for the target INSERT.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion _build_target_columns\n" + }, + { + "contract_id": "_build_context_keys", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 103, + "end_line": 114, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _build_context_keys [C:1] [TYPE Function]\ndef _build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context keys for JSON bundled column.\"\"\"\n context_keys = list(job.context_columns or [])\n if (\n job.translation_column\n and job.translation_column != effective_target\n and job.translation_column not in context_keys\n ):\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion _build_context_keys\n" + }, + { + "contract_id": "_build_insert_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 117, + "end_line": 179, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "PURPOSE", + "message": "@PURPOSE is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _build_insert_rows [C:3] [TYPE Function]\ndef _build_insert_rows(\n records: list[TranslationRecord], job: TranslationJob,\n effective_target: str | None, primary_language: str, context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row dicts for the INSERT SQL statement.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data: dict[str, str] = {}\n for key in context_keys:\n val = source_data.get(key)\n context_data[key] = str(val) if val is not None else \"\"\n\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback = dict(base_row)\n if effective_target:\n fallback[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback[job.target_language_column] = primary_language\n fallback[\"is_original\"] = 0\n rows_for_sql.append(fallback)\n return rows_for_sql\n# #endregion _build_insert_rows\n" + }, + { + "contract_id": "_resolve_insert_backend", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 182, + "end_line": 198, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _resolve_insert_backend [C:1] [TYPE Function]\ndef _resolve_insert_backend(\n config_manager: ConfigManager, job: TranslationJob, batch_id: str,\n) -> tuple[str | None, SupersetSqlLabExecutor | None]:\n \"\"\"Resolve the database backend and SQL executor for batch insert.\"\"\"\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend for batch insert\",\n {\"batch_id\": batch_id, \"error\": str(e)})\n return None, None\n dialect = real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n return dialect, executor\n# #endregion _resolve_insert_backend\n" + }, + { + "contract_id": "_generate_insert_sql", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 201, + "end_line": 219, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _generate_insert_sql [C:1] [TYPE Function]\ndef _generate_insert_sql(\n dialect: str, job: TranslationJob, columns: list[str],\n rows_for_sql: list[dict[str, object]], batch_id: str = \"\",\n) -> tuple[str | None, int]:\n \"\"\"Generate INSERT SQL using SQLGenerator.\"\"\"\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect, target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns, rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n return sql, row_count\n except ValueError as e:\n logger.explore(\"SQL generation failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return None, 0\n# #endregion _generate_insert_sql\n" + }, + { + "contract_id": "_execute_insert_sql", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_insert.py", + "start_line": 222, + "end_line": 232, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _execute_insert_sql [C:1] [TYPE Function]\ndef _execute_insert_sql(executor: SupersetSqlLabExecutor, sql: str, batch_id: str, row_count: int) -> None:\n \"\"\"Execute the INSERT SQL via Superset SQL Lab.\"\"\"\n try:\n result = executor.execute_and_poll(sql=sql, max_polls=30, poll_interval_seconds=2.0)\n except Exception as e:\n logger.explore(\"Superset SQL submission failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return\n logger.reason(f\"Batch {batch_id[:12]} inserted {row_count} rows\",\n {\"batch_id\": batch_id, \"rows\": row_count, \"status\": result.get(\"status\")})\n# #endregion _execute_insert_sql\n" + }, + { + "contract_id": "BatchProcessingService", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_batch_proc.py", + "start_line": 1, + "end_line": 189, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "Batch processing for translation: create batch records, classify rows (cache/preview/LLM)," + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], + "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: create batch records, classify rows (cache/preview/LLM),\n# call LLM service, persist TranslationRecord/TranslationLanguage rows.\n# Insert-to-target delegated to _batch_insert.py.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch], [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [DictionaryManager], [LLMTranslationService]\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\nimport time\nimport uuid\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationBatch, TranslationJob, TranslationLanguage, TranslationRecord\nfrom ...services.llm_provider import LLMProviderService\nfrom ._batch_insert import insert_batch_to_target\nfrom ._llm_call import LLMTranslationService\nfrom ._token_budget import estimate_token_budget\nfrom ._utils import _check_translation_cache, _compute_source_hash, _compute_key_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 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(job, batch_rows, preview_edits_cache)\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 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 def _classify(self, job, batch_rows, preview_edits_cache):\n llm_rows, pre_rows = [], []\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n pre_rows.append(row)\n continue\n cl = row.get(\"_cached_lang_values\")\n if cl:\n tls = job.target_languages or [job.target_dialect or \"en\"]\n tls = [str(tls)] if not isinstance(tls, list) else tls\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 cl = row.get(\"_cached_lang_values\")\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=\"und\", 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", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_proc.py", + "start_line": 43, + "end_line": 86, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "TranslationBatch and TranslationRecord rows are created.", + "PRE": "job and batch_rows are valid.", + "PURPOSE": "Process a single batch: create record, classify rows, call LLM, persist.", + "SIDE_EFFECT": "LLM API call; DB writes." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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 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(job, batch_rows, preview_edits_cache)\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 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" + }, + { + "contract_id": "AdaptiveBatchSizer", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_batch_sizer.py", + "start_line": 1, + "end_line": 223, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Adaptive batch sizing for LLM translation — splits source rows into variable-sized" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "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 ...services.llm_provider import LLMProviderService\nfrom ...models.translate import TranslationJob\nfrom ._token_budget import (\n JSON_OVERHEAD_PER_ROW,\n MAX_OUTPUT_HEADROOM,\n OUTPUT_PER_ROW_PER_LANG,\n PROMPT_BASE_TOKENS,\n REASONING_OVERHEAD,\n estimate_token_budget,\n)\nfrom ._utils import estimate_row_tokens\n\n\n# #region AdaptiveBatchSizer [C:3] [TYPE Class]\n# @BRIEF Split source rows into auto-sized batches based on token budget estimates.\nclass AdaptiveBatchSizer:\n \"\"\"Split source rows into auto-sized batches based on token budget estimates.\n\n Each batch is sized so that its total estimated tokens fit within the\n available context window (input budget), accounting for prompt overhead,\n dictionary entries, and output tokens.\n \"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager) -> None:\n self.db = db\n self.config_manager = config_manager\n\n # #region resolve_provider_model [C:2] [TYPE Function] [SEMANTICS llm, provider, model]\n # @BRIEF Resolve the LLM provider model name for token budget estimation.\n # @POST Returns model name string or None if resolution fails.\n # @SIDE_EFFECT DB query to LLM provider table.\n def resolve_provider_model(self, job: TranslationJob) -> str | None:\n \"\"\"Resolve the provider model name for token budget estimation.\"\"\"\n if not job.provider_id:\n return None\n try:\n p_svc = LLMProviderService(self.db)\n p = p_svc.get_provider(job.provider_id)\n if p:\n return p.default_model or \"gpt-4o-mini\"\n except Exception:\n pass\n return None\n # #endregion resolve_provider_model\n\n # #region auto_size_batches [C:3] [TYPE Function] [SEMANTICS translate, batch, sizing]\n # @BRIEF Split source rows into variable-sized batches based on content length.\n # @PRE source_rows is non-empty. job has valid config.\n # @POST Returns list of batches, each batch is a list of row dicts.\n # Each batch fits within the estimated token budget for its rows.\n # @SIDE_EFFECT DB query to resolve provider model. Logs batch statistics.\n def auto_size_batches(\n self,\n job: TranslationJob,\n source_rows: list[dict],\n target_languages: list[str],\n provider_info: str | None = None,\n ) -> list[list[dict]]:\n \"\"\"Split source rows into auto-sized batches based on content length.\n\n Each batch is sized so that its total estimated tokens fit within the\n available context window (input budget), accounting for prompt overhead,\n dictionary entries, and output tokens.\n \"\"\"\n with belief_scope(\"AdaptiveBatchSizer.auto_size_batches\"):\n if not source_rows:\n return []\n\n if provider_info is None:\n provider_info = self.resolve_provider_model(job)\n\n # 1. Estimate per-row token counts\n row_tokens: list[int] = []\n for row in source_rows:\n source_text = row.get(\"source_text\", \"\")\n source_data = row.get(\"source_data\")\n tokens = estimate_row_tokens(source_text, source_data, job)\n row_tokens.append(tokens)\n\n # 2. Get budget recommendation\n budget = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column or \"source_text\",\n context_columns=job.context_columns,\n batch_size=len(source_rows),\n provider_info=provider_info,\n )\n\n recommended = budget.get(\"batch_size_adjusted\", 0)\n\n # Fallback: if budget calculation fails, use fixed size\n if recommended <= 0:\n fallback_size = job.batch_size or 50\n logger.explore(\"Token budget returned zero — falling back to fixed batch size\", {\n \"fallback_size\": fallback_size,\n \"total_rows\": len(source_rows),\n })\n return [\n source_rows[i:i + fallback_size]\n for i in range(0, len(source_rows), fallback_size)\n ]\n\n # 3. Compute per-batch row-content budget\n # Use the ACTUAL available input capacity (context_window - max_output_tokens),\n # NOT the sum of the first N rows. This prevents long rows from being\n # incorrectly placed in 1-row batches when the context window has plenty of room.\n available_input = budget.get(\"available_input_budget\")\n if available_input is not None:\n # New-style: use actual available input capacity\n per_batch_budget = available_input - PROMPT_BASE_TOKENS\n else:\n # Fallback for tests: use estimated_input (sum of first N rows)\n estimated_input = budget.get(\"estimated_input_tokens\", 50000)\n per_batch_budget = estimated_input - PROMPT_BASE_TOKENS\n\n if per_batch_budget <= 0:\n fallback_size = job.batch_size or 50\n logger.explore(\"Per-batch budget collapsed — falling back to fixed batch size\", {\n \"per_batch_budget\": per_batch_budget,\n \"fallback_size\": fallback_size,\n \"total_rows\": len(source_rows),\n })\n return [\n source_rows[i:i + fallback_size]\n for i in range(0, len(source_rows), fallback_size)\n ]\n\n # 4. Greedy batch splitting\n # Compute max rows per batch from OUTPUT constraint.\n # This prevents output truncation (finish_reason=length) when batching\n # many short rows within the large input budget.\n num_languages = len(target_languages)\n max_output_tokens_val = budget.get(\"max_output_tokens\")\n if max_output_tokens_val is not None:\n output_per_row = num_languages * OUTPUT_PER_ROW_PER_LANG + JSON_OVERHEAD_PER_ROW\n available_output = max_output_tokens_val - REASONING_OVERHEAD - MAX_OUTPUT_HEADROOM\n max_rows_by_output = max(available_output // output_per_row, 1) if output_per_row > 0 else 20\n max_rows_hard_cap = max_rows_by_output\n else:\n # Fallback for tests: old formula\n max_rows_hard_cap = max(recommended * 2, 20)\n\n # 5. Respect job.batch_size as the absolute maximum rows per batch.\n # User-configured batch_size overrides model-based estimates to\n # prevent LLM quality degradation on large batches.\n if job.batch_size:\n max_rows_hard_cap = min(max_rows_hard_cap, job.batch_size)\n\n batches: list[list[dict]] = []\n current_batch: list[dict] = []\n current_tokens = 0\n\n for i, row in enumerate(source_rows):\n rt = row_tokens[i]\n\n if rt > per_batch_budget:\n if current_batch:\n batches.append(current_batch)\n current_batch = []\n current_tokens = 0\n logger.reason(\"Single row exceeds per-batch token budget — placing in own batch\", {\n \"row_index\": i,\n \"row_tokens\": rt,\n \"per_batch_budget\": per_batch_budget,\n })\n batches.append([row])\n continue\n\n should_split = bool(\n current_batch\n and (current_tokens + rt > per_batch_budget\n or len(current_batch) >= max_rows_hard_cap)\n )\n\n if should_split:\n batches.append(current_batch)\n current_batch = [row]\n current_tokens = rt\n else:\n current_batch.append(row)\n current_tokens += rt\n\n if current_batch:\n batches.append(current_batch)\n\n # 5. Log adaptive batch statistics\n if batches:\n avg_size = sum(len(b) for b in batches) / max(1, len(batches))\n max_size = max(len(b) for b in batches)\n logger.reason(\"Auto-sized batches\", {\n \"total_rows\": len(source_rows),\n \"num_batches\": len(batches),\n \"avg_batch_size\": round(avg_size, 1),\n \"max_batch_size\": max_size,\n \"recommended_batch_size\": recommended,\n \"per_batch_budget\": per_batch_budget,\n })\n\n return batches\n # #endregion auto_size_batches\n# #endregion AdaptiveBatchSizer\n# #endregion AdaptiveBatchSizer\n" + }, + { + "contract_id": "resolve_provider_model", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_sizer.py", + "start_line": 48, + "end_line": 64, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Returns model name string or None if resolution fails.", + "PURPOSE": "Resolve the LLM provider model name for token budget estimation.", + "SIDE_EFFECT": "DB query to LLM provider table." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "auto_size_batches", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_batch_sizer.py", + "start_line": 66, + "end_line": 221, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Returns list of batches, each batch is a list of row dicts.", + "PRE": "source_rows is non-empty. job has valid config.", + "PURPOSE": "Split source rows into variable-sized batches based on content length." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "LLMTranslationService", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_llm_call.py", + "start_line": 1, + "end_line": 272, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "LLM interaction for batch translation: call provider with retry, handle truncation" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], + "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# @LAYER Domain\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [ContextAwarePromptBuilder]\n# @RELATION DEPENDS_ON -> [_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\nimport json\nimport time\nimport uuid\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import 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 left = self.call_llm_for_batch(job, run_id, batch_rows[:mid], dict_matches, batch_id + \"_L\", max_tokens, depth + 1)\n right = self.call_llm_for_batch(job, run_id, batch_rows[mid:], dict_matches, batch_id + \"_R\", max_tokens, depth + 1)\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 detected_lang = td.get(\"detected_source_language\", \"und\") if td else \"und\"\n if td is None:\n skipped += 1\n self._add_skipped(row, run_id, batch_id, source_text, \"NULL translation\")\n continue\n plv = self._extract_per_lang_values(td, target_languages)\n if dict_matches and source_text:\n _enforce_dictionary(source_text, plv, dict_matches, batch_id, row_id)\n if not plv:\n skipped += 1\n self._add_skipped(row, run_id, batch_id, source_text, \"Empty translation\")\n continue\n successful += 1\n primary = next(iter(plv.values()), \"\")\n record = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=source_text, target_sql=primary,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n for lang_code in target_languages:\n if detected_lang != \"und\" and str(lang_code).lower() == str(detected_lang).lower():\n continue\n val = plv.get(lang_code, \"\")\n needs_review = (detected_lang == \"und\")\n if needs_review:\n logger.explore(\"undetected language\", {\"record_id\": row_id, \"language_code\": lang_code, \"text\": source_text[:100]})\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=record.id, language_code=lang_code,\n source_language_detected=detected_lang, translated_value=val or \"\",\n final_value=val or \"\", status=\"translated\", needs_review=needs_review,\n ))\n return {\"successful\": successful, \"failed\": failed, \"skipped\": skipped, \"retries\": retries}\n\n @staticmethod\n def _extract_per_lang_values(td, target_languages):\n plv = {}\n has_any = False\n for lc in target_languages:\n lv = td.get(lc)\n if lv is not None and str(lv).strip():\n plv[lc] = str(lv)\n has_any = True\n if not has_any:\n t = td.get(\"translation\", \"\")\n if t.strip():\n plv[target_languages[0]] = t\n has_any = True\n return plv if has_any else {}\n\n def _add_skipped(self, row, run_id, batch_id, source_text, reason):\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=source_text, target_sql=\"\",\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"SKIPPED\",\n error_message=reason,\n ))\n\n # #region call_llm [C:3] [TYPE Function] [SEMANTICS translate, llm, call]\n # @BRIEF Route to provider-specific LLM call implementation.\n def call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> tuple[str, str | None]:\n \"\"\"Call the configured LLM provider with the batch prompt.\"\"\"\n with belief_scope(\"LLMTranslationService.call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n disable_reasoning = getattr(job, 'disable_reasoning', False)\n\n if provider_type not in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\", \"litellm\"):\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n return call_openai_compatible(\n base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt,\n provider_type=provider_type, max_tokens=max_tokens, disable_reasoning=disable_reasoning,\n )\n # #endregion call_llm\n\n # -- Static methods delegated to sub-modules for backward compat --\n @staticmethod\n def call_openai_compatible(*a, **kw):\n return call_openai_compatible(*a, **kw)\n\n @staticmethod\n def _parse_llm_response(*a, **kw):\n return parse_llm_response(*a, **kw)\n# #endregion LLMTranslationService\n# #endregion LLMTranslationService\n" + }, + { + "contract_id": "call_llm_for_batch", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_call.py", + "start_line": 46, + "end_line": 82, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Returns dict with successful/failed/skipped counts.", + "PRE": "job has valid provider_id. batch_rows is non-empty.", + "PURPOSE": "Call LLM for a batch of rows requiring translation. Parse and persist results.", + "SIDE_EFFECT": "HTTP call to LLM provider; DB writes." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "call_llm", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_call.py", + "start_line": 237, + "end_line": 261, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Route to provider-specific LLM call implementation." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "LLMHttpClient", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_llm_http.py", + "start_line": 1, + "end_line": 187, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "HTTP client for OpenAI-compatible LLM API calls with rate-limit handling and" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region LLMHttpClient [C:3] [TYPE Module] [SEMANTICS translate, llm, http, openai, retry, rate-limit]\n# @BRIEF HTTP client for OpenAI-compatible LLM API calls with rate-limit handling and\n# structured output fallback. Extracted from _llm_call.py for INV_7 compliance.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:requests]\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason) tuple.\n# @SIDE_EFFECT HTTP POST to LLM API with optional retry on 429.\n# @RATIONALE Extracted from LLMTranslationService (793 lines) to keep module under INV_7 limit.\n# @REJECTED Single HTTP client class — kept as module-level functions for stateless reusability.\n\nimport time\nfrom typing import Any\n\nfrom ...core.logger import logger\n\n\n# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]\n# @BRIEF Call OpenAI-compatible API with rate-limit handling and structured output fallback.\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason).\n# @SIDE_EFFECT HTTP POST to LLM API.\ndef call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n) -> tuple[str, str | None]:\n \"\"\"Call OpenAI-compatible API for batch translation.\"\"\"\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n\n response, response_text = _do_http_request(url, headers, payload)\n _handle_response_format_fallback(response, response_text, payload, url, headers)\n\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"body={response_text[:2000]}\"\n )\n response.raise_for_status()\n data = response.json()\n\n choices = data.get(\"choices\", [])\n if not choices:\n logger.explore(\"LLM returned no choices\", extra={\n \"src\": \"executor\", \"response_keys\": list(data.keys()),\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\", \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__, \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\", \"refusal\": str(refusal)[:500], \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n\n logger.reason(\n f\"LLM response finish_reason={finish_reason} content_len={len(content)} \"\n f\"msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\"\n )\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\n \"src\": \"executor\", \"finish_reason\": finish_reason,\n \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [],\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n# #endregion call_openai_compatible\n\n\n# #region _do_http_request [C:1] [TYPE Function]\ndef _do_http_request(url: str, headers: dict, payload: dict) -> tuple[Any, str]:\n \"\"\"Make HTTP POST with rate-limit (429) retry handling.\"\"\"\n import requests as http_requests\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response_text = response.text\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait})\n time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n return response, response_text\n# #endregion _do_http_request\n\n\n# #region _handle_response_format_fallback [C:1] [TYPE Function]\ndef _handle_response_format_fallback(\n response: Any, response_text: str, payload: dict, url: str, headers: dict,\n) -> None:\n \"\"\"Handle 400 errors from structured_outputs not being supported.\"\"\"\n import requests as http_requests\n _patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response_text or \"\").lower() for p in _patterns)\n ):\n logger.explore(\"Structured outputs not supported, retrying without response_format\",\n extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n new_response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response.status_code = new_response.status_code\n response._content = new_response.content\n response.encoding = new_response.encoding\n response.headers = new_response.headers\n# #endregion _handle_response_format_fallback\n# #endregion LLMHttpClient\n" + }, + { + "contract_id": "call_openai_compatible", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_http.py", + "start_line": 18, + "end_line": 133, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Returns (response text, finish_reason).", + "PRE": "Valid API endpoint, key, model, and prompt.", + "PURPOSE": "Call OpenAI-compatible API with rate-limit handling and structured output fallback.", + "SIDE_EFFECT": "HTTP POST to LLM API." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]\n# @BRIEF Call OpenAI-compatible API with rate-limit handling and structured output fallback.\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason).\n# @SIDE_EFFECT HTTP POST to LLM API.\ndef call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n) -> tuple[str, str | None]:\n \"\"\"Call OpenAI-compatible API for batch translation.\"\"\"\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n\n response, response_text = _do_http_request(url, headers, payload)\n _handle_response_format_fallback(response, response_text, payload, url, headers)\n\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"body={response_text[:2000]}\"\n )\n response.raise_for_status()\n data = response.json()\n\n choices = data.get(\"choices\", [])\n if not choices:\n logger.explore(\"LLM returned no choices\", extra={\n \"src\": \"executor\", \"response_keys\": list(data.keys()),\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\", \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__, \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\", \"refusal\": str(refusal)[:500], \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n\n logger.reason(\n f\"LLM response finish_reason={finish_reason} content_len={len(content)} \"\n f\"msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\"\n )\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\n \"src\": \"executor\", \"finish_reason\": finish_reason,\n \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [],\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n# #endregion call_openai_compatible\n" + }, + { + "contract_id": "_do_http_request", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_http.py", + "start_line": 136, + "end_line": 163, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _do_http_request [C:1] [TYPE Function]\ndef _do_http_request(url: str, headers: dict, payload: dict) -> tuple[Any, str]:\n \"\"\"Make HTTP POST with rate-limit (429) retry handling.\"\"\"\n import requests as http_requests\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response_text = response.text\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait})\n time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n return response, response_text\n# #endregion _do_http_request\n" + }, + { + "contract_id": "_handle_response_format_fallback", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_http.py", + "start_line": 166, + "end_line": 186, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _handle_response_format_fallback [C:1] [TYPE Function]\ndef _handle_response_format_fallback(\n response: Any, response_text: str, payload: dict, url: str, headers: dict,\n) -> None:\n \"\"\"Handle 400 errors from structured_outputs not being supported.\"\"\"\n import requests as http_requests\n _patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response_text or \"\").lower() for p in _patterns)\n ):\n logger.explore(\"Structured outputs not supported, retrying without response_format\",\n extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n new_response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response.status_code = new_response.status_code\n response._content = new_response.content\n response.encoding = new_response.encoding\n response.headers = new_response.headers\n# #endregion _handle_response_format_fallback\n" + }, + { + "contract_id": "LLMResponseParser", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_llm_parse.py", + "start_line": 1, + "end_line": 117, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Parse LLM JSON response into per-row translations with support for markdown code" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region LLMResponseParser [C:3] [TYPE Module] [SEMANTICS translate, llm, parse, json, recovery]\n# @BRIEF Parse LLM JSON response into per-row translations with support for markdown code\n# blocks, truncated JSON recovery, and multi-language format.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RATIONALE Extracted from _llm_call.py (793 lines) to comply with INV_7 (< 400 lines/module).\n# @REJECTED Inline parsing inside call_llm_for_batch — made the function 327 lines.\n\nimport json\nimport re\nfrom typing import Any\n\nfrom ...core.logger import logger\n\n\n# #region parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate, llm, parse, json]\n# @BRIEF Parse LLM JSON response into dict of row_id -> per-language translations.\n# @PRE response_text is valid JSON with {\"rows\": [...]} structure.\n# @POST Returns dict mapping row_id to dict with 'detected_source_language' and per-language codes.\n# @RAISES ValueError if JSON cannot be parsed and no rows recovered.\ndef parse_llm_response(\n response_text: str,\n expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n) -> dict[str, dict[str, str]]:\n \"\"\"Parse LLM JSON response into dict of row_id -> translations.\"\"\"\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n data = _recover_from_markdown(response_text)\n\n if data is None:\n data = _recover_truncated_rows(response_text, expected_count, finish_reason)\n\n if data is None:\n logger.explore(\"No complete rows found in truncated response\",\n extra={\"src\": \"executor\", \"response_preview\": response_text[:1000]})\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: dict[str, dict[str, str]] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n if not row_id:\n continue\n\n detected_lang = str(item.get(\"detected_source_language\", \"und\")) if item.get(\"detected_source_language\") else \"und\"\n result: dict[str, str] = {\"detected_source_language\": detected_lang}\n\n has_language_data = False\n if target_languages:\n for lang_code in target_languages:\n lang_val = item.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n result[lang_code] = str(lang_val)\n has_language_data = True\n\n if not has_language_data:\n translation = item.get(\"translation\")\n if translation is not None:\n result[\"translation\"] = str(translation)\n has_language_data = True\n\n if has_language_data:\n translations[row_id] = result\n\n if len(translations) < expected_count:\n logger.explore(f\"LLM returned fewer translations expected={expected_count} got={len(translations)}\")\n\n return translations\n# #endregion parse_llm_response\n\n\n# #region _recover_from_markdown [C:1] [TYPE Function]\ndef _recover_from_markdown(response_text: str) -> dict | None:\n \"\"\"Try to extract JSON from markdown code block.\"\"\"\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n rows = data.get(\"rows\", [])\n if isinstance(rows, list) and rows:\n logger.reason(\"Parsed JSON from markdown code block\", {\"rows\": len(rows)})\n return data\n except json.JSONDecodeError:\n pass\n return None\n# #endregion _recover_from_markdown\n\n\n# #region _recover_truncated_rows [C:1] [TYPE Function]\ndef _recover_truncated_rows(\n response_text: str, expected_count: int, finish_reason: str | None,\n) -> dict | None:\n \"\"\"Try to recover complete rows from truncated JSON response.\"\"\"\n logger.explore(\"LLM truncated, trying partial row recovery\",\n extra={\"src\": \"executor\", \"finish_reason\": finish_reason, \"response_length\": len(response_text)})\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n row_data = json.loads(row_text)\n partial_rows.append(row_data)\n except json.JSONDecodeError:\n continue\n if partial_rows:\n logger.explore(f\"Recovered {len(partial_rows)}/{expected_count} complete rows from truncated response\",\n extra={\"src\": \"executor\"})\n return {\"rows\": partial_rows}\n return None\n# #endregion _recover_truncated_rows\n# #endregion LLMResponseParser\n" + }, + { + "contract_id": "_recover_from_markdown", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_parse.py", + "start_line": 78, + "end_line": 92, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _recover_from_markdown [C:1] [TYPE Function]\ndef _recover_from_markdown(response_text: str) -> dict | None:\n \"\"\"Try to extract JSON from markdown code block.\"\"\"\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n rows = data.get(\"rows\", [])\n if isinstance(rows, list) and rows:\n logger.reason(\"Parsed JSON from markdown code block\", {\"rows\": len(rows)})\n return data\n except json.JSONDecodeError:\n pass\n return None\n# #endregion _recover_from_markdown\n" + }, + { + "contract_id": "_recover_truncated_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_llm_parse.py", + "start_line": 95, + "end_line": 116, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _recover_truncated_rows [C:1] [TYPE Function]\ndef _recover_truncated_rows(\n response_text: str, expected_count: int, finish_reason: str | None,\n) -> dict | None:\n \"\"\"Try to recover complete rows from truncated JSON response.\"\"\"\n logger.explore(\"LLM truncated, trying partial row recovery\",\n extra={\"src\": \"executor\", \"finish_reason\": finish_reason, \"response_length\": len(response_text)})\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n row_data = json.loads(row_text)\n partial_rows.append(row_data)\n except json.JSONDecodeError:\n continue\n if partial_rows:\n logger.explore(f\"Recovered {len(partial_rows)}/{expected_count} complete rows from truncated response\",\n extra={\"src\": \"executor\"})\n return {\"rows\": partial_rows}\n return None\n# #endregion _recover_truncated_rows\n" + }, + { + "contract_id": "RunExecutionService", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_run_service.py", + "start_line": 1, + "end_line": 176, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "Full run lifecycle: prepare run, fetch source rows, filter new keys, orchestrate batches," + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region RunExecutionService [C:4] [TYPE Module] [SEMANTICS translate, run, execution, orchestration]\n# @BRIEF Full run lifecycle: prepare run, fetch source rows, filter new keys, orchestrate batches,\n# handle cancellation, update per-language stats, finalize run status.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRun], [TranslationJob], [TranslationBatch], [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession], [BatchProcessingService], [AdaptiveBatchSizer]\n# @RELATION DEPENDS_ON -> [RunSourceFetcher]\n# @PRE DB session available. Run has valid job config.\n# @POST Run status updated to COMPLETED/FAILED/CANCELLED. Batches and records created.\n# @SIDE_EFFECT Fetches data from Superset API; calls BatchProcessingService; creates DB rows.\n# @RATIONALE Extracted from TranslationExecutor. Source fetching delegated to _run_source.py.\n# @REJECTED Keeping all run orchestration inside TranslationExecutor — caused class to exceed INV_7.\n\nfrom collections.abc import Callable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRecord, TranslationRun, TranslationRunLanguageStats\nfrom ._batch_sizer import AdaptiveBatchSizer\nfrom ._run_source import _extract_chart_data_rows, fetch_source_rows\n\n\n# #region RunExecutionService [C:4] [TYPE Class]\n# @BRIEF Orchestrate full translation run: fetch data, process batches, finalize.\nclass RunExecutionService:\n \"\"\"Orchestrate a full translation run: prepare, process batches, finalize.\"\"\"\n\n def __init__(\n self, db: Session, config_manager: ConfigManager,\n current_user: str | None = None,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n ) -> None:\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.on_batch_progress = on_batch_progress\n self._preview_edits_cache: dict[str, dict[str, str]] | None = None\n\n # -- Source fetching (thin wrappers) --\n def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:\n return fetch_source_rows(self.db, self.config_manager, job_id, run_id)\n\n @staticmethod\n def _extract_chart_data_rows(response):\n return _extract_chart_data_rows(response)\n\n # -- Key/filter helpers --\n def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:\n \"\"\"Filter source rows to only new keys not yet translated.\"\"\"\n with belief_scope(\"RunExecutionService._filter_new_keys\"):\n prev_run = (\n self.db.query(TranslationRun)\n .filter(TranslationRun.job_id == job.id, TranslationRun.status == \"COMPLETED\",\n TranslationRun.insert_status == \"succeeded\", TranslationRun.id != run_id)\n .order_by(TranslationRun.created_at.desc()).first()\n )\n if not prev_run:\n logger.reason(\"No prior successful run — all keys treated as new\", {\"job_id\": job.id})\n return source_rows\n\n prev_records = (\n self.db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == prev_run.id, TranslationRecord.status == \"SUCCESS\")\n .all()\n )\n if not prev_records:\n return source_rows\n\n key_cols = job.target_key_cols or job.source_key_cols or []\n if not key_cols:\n logger.explore(\"No key columns configured — skipping new-key-only filter\", {\"job_id\": job.id})\n return source_rows\n\n existing_keys = set()\n for rec in prev_records:\n sd = rec.source_data or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n existing_keys.add(key_tuple)\n\n filtered = []\n skipped = 0\n for row in source_rows:\n sd = row.get(\"source_data\", {}) or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n if key_tuple not in existing_keys:\n filtered.append(row)\n else:\n skipped += 1\n\n logger.reason(f\"New-key-only filter: {len(source_rows)} total -> {len(filtered)} new, {skipped} skipped\",\n {\"job_id\": job.id, \"prev_run_id\": prev_run.id, \"key_cols\": key_cols})\n return filtered\n\n def _load_preview_edits(self, job_id: str) -> None:\n \"\"\"Load preview edits for carry-forward during execution.\"\"\"\n from ...models.translate import TranslationPreviewLanguage, TranslationPreviewRecord, TranslationPreviewSession\n\n with belief_scope(\"RunExecutionService._load_preview_edits\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id, TranslationPreviewSession.status == \"APPLIED\")\n .order_by(TranslationPreviewSession.created_at.desc()).first()\n )\n if not session:\n logger.reason(\"No applied preview session found — no edits to carry forward\", {\"job_id\": job_id})\n self._preview_edits_cache = {}\n return\n\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id).all()\n )\n edits: dict[str, dict[str, str]] = {}\n for rec in records:\n if not rec.source_data:\n continue\n key_hash = self._compute_key_hash(rec.source_data)\n edited_langs: dict[str, str] = {}\n for lang_entry in (rec.languages or []):\n if lang_entry.status in (\"edited\", \"approved\") and lang_entry.user_edit:\n edited_langs[lang_entry.language_code] = lang_entry.final_value or lang_entry.user_edit\n logger.reason(\"Carrying forward preview edit\", {\"key_hash\": key_hash, \"language_code\": lang_entry.language_code})\n if edited_langs:\n edits[key_hash] = edited_langs\n self._preview_edits_cache = edits\n logger.reason(f\"Loaded {len(edits)} preview edits for carry-forward\", {\"job_id\": job_id})\n\n @staticmethod\n def _compute_key_hash(source_data: dict) -> str:\n import hashlib, json\n return hashlib.sha256(json.dumps(source_data, sort_keys=True).encode()).hexdigest()[:16]\n\n # -- Language stats --\n def _update_language_stats_incremental(self, run_id: str, language_stats_map: dict[str, TranslationRunLanguageStats]) -> None:\n \"\"\"Update per-language statistics incrementally after each batch.\"\"\"\n with belief_scope(\"RunExecutionService._update_language_stats_incremental\"):\n records = self.db.query(TranslationRecord).filter(TranslationRecord.run_id == run_id).all()\n record_ids = [r.id for r in records]\n if not record_ids:\n return\n from ...models.translate import TranslationLanguage\n lang_entries = (\n self.db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids)).all()\n )\n from collections import defaultdict\n agg: dict[str, dict[str, int]] = defaultdict(lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_tokens = max(1, sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value) // 4)\n num_langs = len(language_stats_map) or 1\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n self.db.flush()\n# #endregion RunExecutionService\n# #endregion RunExecutionService\n" + }, + { + "contract_id": "RunSourceFetcher", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_run_source.py", + "start_line": 1, + "end_line": 141, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Fetch source rows for translation runs from Superset datasource or preview session." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region RunSourceFetcher [C:3] [TYPE Module] [SEMANTICS translate, source, fetch, superset, preview]\n# @BRIEF Fetch source rows for translation runs from Superset datasource or preview session.\n# Extracted from RunExecutionService to comply with INV_7 (< 400 lines/module).\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob], [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE job_id exists in DB.\n# @POST Returns list of dicts with source data (text, row_index, approved_translation, source_data).\n# @SIDE_EFFECT Makes HTTP call to Superset chart data API when datasource is configured.\n# @RATIONALE Extracted from RunExecutionService (620 lines) — _fetch_source_rows alone was ~100 lines.\n# @REJECTED Inline fetch logic in execute_run — made execute_run exceed 150 lines.\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationPreviewRecord, TranslationPreviewSession\n\nMAX_ROWS_PER_RUN = 10000\n\n\n# #region fetch_source_rows [C:3] [TYPE Function] [SEMANTICS translate, source, fetch]\n# @BRIEF Fetch source rows from Superset datasource or preview session fallback.\ndef fetch_source_rows(db: Session, config_manager: ConfigManager, job_id: str, run_id: str) -> list[dict[str, Any]]:\n \"\"\"Fetch source rows from Superset datasource or preview session fallback.\"\"\"\n with belief_scope(\"RunSourceFetcher.fetch_source_rows\"):\n job = db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n\n if job and job.source_datasource_id:\n try:\n logger.reason(\"Fetching full dataset from Superset datasource\", {\n \"run_id\": run_id, \"datasource_id\": job.source_datasource_id,\n \"environment_id\": job.environment_id,\n })\n environments = config_manager.get_environments()\n target_env_id = job.environment_id or job.source_dialect or \"\"\n env_config = next((e for e in environments if e.id == target_env_id), None)\n if not env_config and environments:\n env_config = environments[0]\n\n if env_config:\n from ...core.superset_client import SupersetClient\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail,\n template_params={}, effective_filters=[],\n )\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = MAX_ROWS_PER_RUN\n queries[0].pop(\"result_type\", None)\n queries[0][\"metrics\"] = []\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n response = client.network.request(\n method=\"POST\", endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context), headers={\"Content-Type\": \"application/json\"},\n )\n rows = _extract_chart_data_rows(response)\n if rows:\n logger.reason(f\"Fetched {len(rows)} rows from Superset datasource\", {\"run_id\": run_id})\n source_rows = []\n for idx, row in enumerate(rows):\n sd = dict(row) if row else None\n st = str(row.get(job.translation_column, \"\")) if job.translation_column else json.dumps(row)\n source_rows.append({\n \"row_index\": str(idx), \"source_text\": st,\n \"approved_translation\": None, \"source_object_name\": f\"Row {idx}\",\n \"source_data\": sd,\n })\n return source_rows\n logger.explore(\"Superset datasource returned no rows\", {\"run_id\": run_id, \"datasource_id\": job.source_datasource_id})\n else:\n logger.explore(\"No environment config found\", {\"env_id\": target_env_id})\n except Exception as e:\n logger.explore(\"Failed to fetch full dataset from Superset, falling back to preview\",\n {\"run_id\": run_id, \"error\": str(e)})\n\n session = (\n db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id, TranslationPreviewSession.status == \"APPLIED\")\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n logger.explore(\"No accepted preview session found\", {\"job_id\": job_id})\n return []\n\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id,\n TranslationPreviewRecord.status.in_([\"APPROVED\", \"PENDING\"]))\n .all()\n )\n source_rows = []\n for rec in records:\n sd = None\n if hasattr(rec, \"source_data\") and rec.source_data:\n sd = dict(rec.source_data)\n source_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": rec.target_sql if rec.status == \"APPROVED\" else None,\n \"source_object_name\": rec.source_object_name or \"\",\n \"source_data\": sd,\n })\n logger.reason(f\"Fetched {len(source_rows)} source rows from preview fallback\",\n {\"run_id\": run_id, \"session_id\": session.id})\n return source_rows\n# #endregion fetch_source_rows\n\n\n# #region _extract_chart_data_rows [C:1] [TYPE Function]\ndef _extract_chart_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n \"\"\"Extract data rows from Superset chart data API response.\"\"\"\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, list):\n return result\n return []\n# #endregion _extract_chart_data_rows\n# #endregion RunSourceFetcher\n" + }, + { + "contract_id": "fetch_source_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_run_source.py", + "start_line": 26, + "end_line": 117, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Fetch source rows from Superset datasource or preview session fallback." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region fetch_source_rows [C:3] [TYPE Function] [SEMANTICS translate, source, fetch]\n# @BRIEF Fetch source rows from Superset datasource or preview session fallback.\ndef fetch_source_rows(db: Session, config_manager: ConfigManager, job_id: str, run_id: str) -> list[dict[str, Any]]:\n \"\"\"Fetch source rows from Superset datasource or preview session fallback.\"\"\"\n with belief_scope(\"RunSourceFetcher.fetch_source_rows\"):\n job = db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n\n if job and job.source_datasource_id:\n try:\n logger.reason(\"Fetching full dataset from Superset datasource\", {\n \"run_id\": run_id, \"datasource_id\": job.source_datasource_id,\n \"environment_id\": job.environment_id,\n })\n environments = config_manager.get_environments()\n target_env_id = job.environment_id or job.source_dialect or \"\"\n env_config = next((e for e in environments if e.id == target_env_id), None)\n if not env_config and environments:\n env_config = environments[0]\n\n if env_config:\n from ...core.superset_client import SupersetClient\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail,\n template_params={}, effective_filters=[],\n )\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = MAX_ROWS_PER_RUN\n queries[0].pop(\"result_type\", None)\n queries[0][\"metrics\"] = []\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n response = client.network.request(\n method=\"POST\", endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context), headers={\"Content-Type\": \"application/json\"},\n )\n rows = _extract_chart_data_rows(response)\n if rows:\n logger.reason(f\"Fetched {len(rows)} rows from Superset datasource\", {\"run_id\": run_id})\n source_rows = []\n for idx, row in enumerate(rows):\n sd = dict(row) if row else None\n st = str(row.get(job.translation_column, \"\")) if job.translation_column else json.dumps(row)\n source_rows.append({\n \"row_index\": str(idx), \"source_text\": st,\n \"approved_translation\": None, \"source_object_name\": f\"Row {idx}\",\n \"source_data\": sd,\n })\n return source_rows\n logger.explore(\"Superset datasource returned no rows\", {\"run_id\": run_id, \"datasource_id\": job.source_datasource_id})\n else:\n logger.explore(\"No environment config found\", {\"env_id\": target_env_id})\n except Exception as e:\n logger.explore(\"Failed to fetch full dataset from Superset, falling back to preview\",\n {\"run_id\": run_id, \"error\": str(e)})\n\n session = (\n db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id, TranslationPreviewSession.status == \"APPLIED\")\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n logger.explore(\"No accepted preview session found\", {\"job_id\": job_id})\n return []\n\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id,\n TranslationPreviewRecord.status.in_([\"APPROVED\", \"PENDING\"]))\n .all()\n )\n source_rows = []\n for rec in records:\n sd = None\n if hasattr(rec, \"source_data\") and rec.source_data:\n sd = dict(rec.source_data)\n source_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": rec.target_sql if rec.status == \"APPROVED\" else None,\n \"source_object_name\": rec.source_object_name or \"\",\n \"source_data\": sd,\n })\n logger.reason(f\"Fetched {len(source_rows)} source rows from preview fallback\",\n {\"run_id\": run_id, \"session_id\": session.id})\n return source_rows\n# #endregion fetch_source_rows\n" + }, + { + "contract_id": "_extract_chart_data_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_run_source.py", + "start_line": 120, + "end_line": 140, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _extract_chart_data_rows [C:1] [TYPE Function]\ndef _extract_chart_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n \"\"\"Extract data rows from Superset chart data API response.\"\"\"\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, list):\n return result\n return []\n# #endregion _extract_chart_data_rows\n" + }, + { + "contract_id": "estimate_token_budget", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/_token_budget.py", + "start_line": 1, + "end_line": 342, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "Domain", + "PURPOSE": "Calculate safe batch_size and max_tokens for LLM translation calls based on actual content length and model context window limits.", + "RATIONALE": "Prevents LLM truncation (finish_reason=length) by sizing batches within context limits." + }, + "relations": [ + { + "source_id": "estimate_token_budget", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreview:Module", + "target_ref": "[TranslationPreview:Module]" + }, + { + "source_id": "estimate_token_budget", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationExecutor:Module", + "target_ref": "[TranslationExecutor:Module]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region estimate_token_budget [C:3] [TYPE Module] [SEMANTICS translate, token, budget, estimation, llm]\n# @BRIEF Calculate safe batch_size and max_tokens for LLM translation calls based on actual content length and model context window limits.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationPreview:Module]\n# @RELATION DEPENDS_ON -> [TranslationExecutor:Module]\n# @RATIONALE: Prevents LLM truncation (finish_reason=length) by sizing batches within context limits.\n# DeepSeek v4 Flash supports up to 64K context window; output is limited by max_tokens.\n# @REJECTED: External tokenizer library — would introduce heavy dependency for estimation only.\n# Fixed batch_size of 50 — causes truncation on long-content rows.\n\n# #region DEFAULT_CONTEXT_WINDOW [TYPE Constant]\n\nDEFAULT_CONTEXT_WINDOW = 64000\n# #endregion DEFAULT_CONTEXT_WINDOW\n# #region DEFAULT_MAX_OUTPUT_TOKENS [TYPE Constant]\n\nDEFAULT_MAX_OUTPUT_TOKENS = 16384\n# #endregion DEFAULT_MAX_OUTPUT_TOKENS\n# @BRIEF CoT reasoning overhead tokens for DeepSeek models (~2000 tokens for chain-of-thought).\n# #region REASONING_OVERHEAD [TYPE Constant]\n\nREASONING_OVERHEAD = 2000\n# #endregion REASONING_OVERHEAD\n\n# #region PROVIDER_DEFAULTS [TYPE Constant]\n\n# Maps model name (or \"default\" fallback) to capacity limits.\n# @RATIONALE Different providers have drastically different context windows and\n# output limits. Using a single default for all causes either wasted\n# capacity (underestimation) or truncation (overestimation).\nPROVIDER_DEFAULTS: dict[str, dict[str, int]] = {\n \"gpt-4o-mini\": {\"context_window\": 128000, \"max_output_tokens\": 16384},\n \"gpt-4o\": {\"context_window\": 128000, \"max_output_tokens\": 16384},\n \"o1-mini\": {\"context_window\": 128000, \"max_output_tokens\": 65536},\n \"claude-3-5-sonnet\": {\"context_window\": 200000, \"max_output_tokens\": 8192},\n \"deepseek-v4-flash\": {\"context_window\": 64000, \"max_output_tokens\": 8192},\n \"default\": {\"context_window\": 64000, \"max_output_tokens\": 16384},\n}\n# #endregion PROVIDER_DEFAULTS\n# Increased from 60 to 120 because SQL/dashboard text and JSON structure need more.\n# #region OUTPUT_PER_ROW_PER_LANG [TYPE Constant]\n\n# Increased from 60 to 120 because SQL/dashboard text and JSON structure need more.\nOUTPUT_PER_ROW_PER_LANG = 120\n# #endregion OUTPUT_PER_ROW_PER_LANG\n# #region JSON_OVERHEAD_PER_ROW [TYPE Constant]\n\nJSON_OVERHEAD_PER_ROW = 50\n# #endregion JSON_OVERHEAD_PER_ROW\n# #region PROMPT_BASE_TOKENS [TYPE Constant]\n\n# Increased from 300 to 600 to account for longer template, system msg, and dict preamble.\nPROMPT_BASE_TOKENS = 600\n# #endregion PROMPT_BASE_TOKENS\n# @BRIEF Cap for dictionary tokens in estimation to avoid overestimating.\n# #region DICT_TOKENS_PER_ENTRY [TYPE Constant]\n\nDICT_TOKENS_PER_ENTRY = 20\n# #endregion DICT_TOKENS_PER_ENTRY\n\n# #region DICT_TOKENS_MAX [TYPE Constant]\n\nDICT_TOKENS_MAX = 5000\n# #endregion DICT_TOKENS_MAX\n# #region CHARS_PER_TOKEN_MIXED [TYPE Constant]\n\nCHARS_PER_TOKEN_MIXED = 2.2\n# #endregion CHARS_PER_TOKEN_MIXED\n# #region MIN_MAX_TOKENS [TYPE Constant]\n\nMIN_MAX_TOKENS = 4096\n# #endregion MIN_MAX_TOKENS\n# #region MAX_OUTPUT_HEADROOM [TYPE Constant]\n\n# Increased from 1000 to 3000 because SQL/dashboard text output varies significantly.\nMAX_OUTPUT_HEADROOM = 3000\n# #endregion MAX_OUTPUT_HEADROOM\n\n\n# region _estimate_tokens_for_text [TYPE Function]\n# @BRIEF Estimate token count for a text string with CJK-aware heuristics.\n# CJK characters (~1.5 chars/token) vs other text (~2.2 chars/token).\n# @PRE: text is a string.\n# @POST: Returns estimated token count >= 1.\n# @RATIONALE: CJK characters are more token-dense than Latin/Cyrillic text.\n# Using a single ratio undercounts CJK input and causes truncation.\n# @REJECTED: Using tiktoken — would introduce a heavy dependency for estimation only.\ndef _estimate_tokens_for_text(text: str) -> int:\n \"\"\"Estimate token count with CJK-aware heuristics.\n\n CJK characters (CJK Unified Ideographs) use ~1.5 chars/token.\n All other characters use ~2.2 chars/token.\n \"\"\"\n if not text:\n return 1\n\n cjk_count = 0\n other_count = 0\n for ch in text:\n if '\\u4e00' <= ch <= '\\u9fff' or '\\u3000' <= ch <= '\\u303f' or '\\uff00' <= ch <= '\\uffef':\n cjk_count += 1\n else:\n other_count += 1\n\n cjk_tokens = cjk_count / 1.5 if cjk_count else 0\n other_tokens = other_count / 2.2 if other_count else 0\n return max(1, int(cjk_tokens + other_tokens))\n# endregion _estimate_tokens_for_text\n\n\n# region _count_rows_that_fit [TYPE Function]\n# @BRIEF Count how many rows fit within the available input budget.\n# @PRE: input_per_row is non-empty; available_budget > 0.\n# @POST: Returns (safe_count, total_input_tokens). When no rows fit, returns (0, 0).\ndef _count_rows_that_fit(\n input_per_row: list[int],\n available_budget: int,\n) -> tuple[int, int]:\n \"\"\"Count consecutive rows that fit within available_budget.\n\n Returns:\n (safe_count, total_input_tokens): Number of rows and their total tokens.\n Returns (0, 0) when the first row alone does not fit (MEDIUM fix).\n \"\"\"\n running_total = 0\n safe_size = 0\n for tokens in input_per_row:\n if running_total + tokens + REASONING_OVERHEAD < available_budget:\n running_total += tokens\n safe_size += 1\n else:\n break\n # MEDIUM: Return (0, 0) when no rows fit — signal upstream that oversizing occurred.\n # Prevents silent clamping to 1 which would produce truncated LLM calls.\n return safe_size, running_total\n# endregion _count_rows_that_fit\n\n\n# region estimate_token_budget [C:3] [TYPE Function]\n# @BRIEF Estimate token budget for a batch of source rows and return safe batch parameters.\n# @PRE: source_rows is a list of dicts (can be empty). target_languages is a non-empty list.\n# @POST: Returns dict with batch_size_adjusted, estimated_input_tokens, estimated_output_tokens, max_output_needed, warning.\n# @RATIONALE: Uses character-count heuristics (chars/2.2 for mixed text) since exact tokenization\n# depends on the LLM model. Estimates are intentionally conservative to prevent truncation.\n# @REJECTED: Using tiktoken or similar tokenizer — would introduce a heavy dependency and still\n# not match DeepSeek's tokenizer exactly.\ndef _calculate_output_tokens(\n safe_size: int,\n num_languages: int,\n) -> int:\n \"\"\"Calculate estimated output tokens for a batch.\"\"\"\n return (\n safe_size * num_languages * OUTPUT_PER_ROW_PER_LANG\n + safe_size * JSON_OVERHEAD_PER_ROW\n + REASONING_OVERHEAD\n )\n\n\ndef _apply_output_aware_batch_sizing(\n safe_size: int,\n num_languages: int,\n max_output_tokens: int,\n) -> int:\n \"\"\"Reduce batch size until estimated output fits within max_output_tokens.\"\"\"\n while safe_size > 0:\n needed_output = (\n safe_size * num_languages * OUTPUT_PER_ROW_PER_LANG\n + safe_size * JSON_OVERHEAD_PER_ROW\n + REASONING_OVERHEAD\n + MAX_OUTPUT_HEADROOM\n )\n if needed_output <= max_output_tokens:\n break\n safe_size -= 1\n return safe_size\n\n\ndef _build_warning(\n batch_size: int | None,\n safe_size: int,\n total_rows: int,\n context_window: int,\n estimated_input: int,\n max_output_needed: int,\n dict_warning: str | None,\n) -> str | None:\n \"\"\"Build warning message when batch size is reduced.\"\"\"\n warning = None\n if batch_size and safe_size < batch_size:\n total_estimated = estimated_input + max_output_needed\n warning = (\n f\"Reduced batch size from {batch_size} to {safe_size} \"\n f\"(estimated {total_estimated} tokens vs {context_window} window)\"\n )\n elif not batch_size and safe_size < total_rows:\n warning = (\n f\"Auto-calculated batch size of {safe_size} from {total_rows} rows \"\n f\"(output-limited)\"\n )\n if dict_warning:\n warning = f\"{warning}; {dict_warning}\" if warning else dict_warning\n return warning\n\n\ndef estimate_token_budget(\n source_rows: list[dict],\n target_languages: list[str],\n source_column: str = \"source_text\",\n context_columns: list[str] | None = None,\n dictionary_entries: list | None = None,\n batch_size: int | None = None,\n context_window: int | None = None,\n max_output_tokens: int | None = None,\n provider_info: str | None = None,\n) -> dict:\n \"\"\"Estimate token budget and return safe batch parameters.\n\n Args:\n source_rows: List of row dicts with source text.\n target_languages: List of target language codes.\n source_column: Key for the source text in each row dict.\n context_columns: Optional list of keys for context columns.\n dictionary_entries: Optional list of dictionary entries for glossary.\n batch_size: Desired batch size. If None, auto-calculate max safe size.\n context_window: Model context window. If None, resolved from provider_info.\n max_output_tokens: Hard max output tokens limit. If None, resolved from provider_info.\n provider_info: Optional provider model name (e.g. \"gpt-4o-mini\") for provider-aware defaults.\n When provided without explicit context_window/max_output_tokens, uses\n PROVIDER_DEFAULTS to set appropriate limits.\n\n Returns:\n dict with:\n batch_size_adjusted: Safe batch size (may be less than requested).\n estimated_input_tokens: Total estimated input tokens for the batch.\n estimated_output_tokens: Total estimated output tokens.\n max_output_needed: Recommended max_tokens for this batch.\n warning: str | None if batch was reduced.\n available_input_budget: Total input token budget per batch (context_window - max_output_tokens).\n max_output_tokens: The effective max output tokens limit used for this batch.\n \"\"\"\n if not target_languages:\n target_languages = [\"en\"]\n\n # Resolve provider-aware defaults\n if provider_info and context_window is None and max_output_tokens is None:\n provider_settings = PROVIDER_DEFAULTS.get(provider_info)\n if provider_settings:\n context_window = provider_settings[\"context_window\"]\n max_output_tokens = provider_settings[\"max_output_tokens\"]\n\n # Fall back to module defaults if still None\n if context_window is None:\n context_window = DEFAULT_CONTEXT_WINDOW\n if max_output_tokens is None:\n max_output_tokens = DEFAULT_MAX_OUTPUT_TOKENS\n\n num_languages = len(target_languages)\n\n # 1. Estimate tokens per row using CJK-aware heuristics\n input_per_row = []\n limit = batch_size if batch_size else len(source_rows)\n for i, row in enumerate(source_rows):\n if i >= limit:\n break\n text = str(row.get(source_column, \"\") or \"\")\n estimated_tokens = _estimate_tokens_for_text(text)\n if context_columns:\n for col in context_columns:\n val = str(row.get(col, \"\") or \"\")\n estimated_tokens += _estimate_tokens_for_text(val)\n input_per_row.append(estimated_tokens)\n\n # 2. Calculate dictionary tokens with warning if capped\n dict_tokens = 0\n dict_warning = None\n if dictionary_entries:\n raw_dict_tokens = len(dictionary_entries) * DICT_TOKENS_PER_ENTRY\n dict_tokens = min(raw_dict_tokens, DICT_TOKENS_MAX)\n if raw_dict_tokens > DICT_TOKENS_MAX:\n dict_warning = (\n f\"Dictionary entries ({len(dictionary_entries)} entries \"\n f\"≈ {raw_dict_tokens} tokens) exceed cap of {DICT_TOKENS_MAX} — \"\n f\"truncated to {DICT_TOKENS_MAX} in estimation\"\n )\n\n prompt_tokens = PROMPT_BASE_TOKENS + dict_tokens\n available_input_budget = context_window - max_output_tokens\n\n # 3. Calculate safe batch size using shared helper\n safe_size, input_total = _count_rows_that_fit(input_per_row, available_input_budget)\n estimated_input = prompt_tokens + input_total\n\n # If batch_size was specified and we reduced it, recalculate\n if batch_size and safe_size < batch_size:\n _, truncated_total = _count_rows_that_fit(\n input_per_row[:batch_size], available_input_budget,\n )\n estimated_input = prompt_tokens + truncated_total\n\n # 4. Estimate output tokens (includes JSON overhead per row)\n estimated_output = _calculate_output_tokens(safe_size, num_languages)\n\n # 5. Output-aware batch sizing: reduce batch if output exceeds limit\n safe_size = _apply_output_aware_batch_sizing(\n safe_size, num_languages, max_output_tokens,\n )\n\n # Ensure at least 1 row per batch — prevents empty batch allocation\n safe_size = max(safe_size, 1)\n\n # Recalculate totals after output-aware reduction\n if safe_size > 0:\n new_input_total = sum(input_per_row[:safe_size])\n estimated_input = prompt_tokens + new_input_total\n estimated_output = _calculate_output_tokens(safe_size, num_languages)\n\n # 6. Calculate recommended max_tokens\n max_output_needed = min(\n estimated_output + MAX_OUTPUT_HEADROOM,\n context_window - estimated_input,\n )\n max_output_needed = max(max_output_needed, MIN_MAX_TOKENS)\n max_output_needed = min(max_output_needed, max_output_tokens)\n\n # 7. Generate warning if batch was reduced\n warning = _build_warning(\n batch_size, safe_size, len(source_rows),\n context_window, estimated_input, max_output_needed,\n dict_warning,\n )\n\n return {\n \"batch_size_adjusted\": safe_size,\n \"estimated_input_tokens\": estimated_input,\n \"estimated_output_tokens\": estimated_output,\n \"max_output_needed\": max_output_needed,\n \"warning\": warning,\n \"available_input_budget\": available_input_budget,\n \"max_output_tokens\": max_output_tokens,\n }\n# endregion estimate_token_budget\n# #endregion estimate_token_budget\n" + }, { "contract_id": "DEFAULT_CONTEXT_WINDOW", "contract_type": "Constant", @@ -48812,8 +51000,8 @@ "contract_id": "DEFAULT_MAX_OUTPUT_TOKENS", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 16, - "end_line": 19, + "start_line": 15, + "end_line": 18, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48827,8 +51015,8 @@ "contract_id": "REASONING_OVERHEAD", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 21, - "end_line": 24, + "start_line": 20, + "end_line": 23, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48842,8 +51030,8 @@ "contract_id": "PROVIDER_DEFAULTS", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 26, - "end_line": 40, + "start_line": 25, + "end_line": 39, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48857,8 +51045,8 @@ "contract_id": "OUTPUT_PER_ROW_PER_LANG", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 42, - "end_line": 46, + "start_line": 41, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48872,8 +51060,8 @@ "contract_id": "JSON_OVERHEAD_PER_ROW", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 48, - "end_line": 51, + "start_line": 46, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48887,8 +51075,8 @@ "contract_id": "PROMPT_BASE_TOKENS", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 53, - "end_line": 57, + "start_line": 50, + "end_line": 54, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48902,8 +51090,8 @@ "contract_id": "DICT_TOKENS_PER_ENTRY", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 59, - "end_line": 62, + "start_line": 56, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48917,8 +51105,8 @@ "contract_id": "DICT_TOKENS_MAX", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 64, - "end_line": 67, + "start_line": 61, + "end_line": 64, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48932,8 +51120,8 @@ "contract_id": "CHARS_PER_TOKEN_MIXED", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 69, - "end_line": 72, + "start_line": 65, + "end_line": 68, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48947,8 +51135,8 @@ "contract_id": "MIN_MAX_TOKENS", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 74, - "end_line": 77, + "start_line": 69, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48962,8 +51150,8 @@ "contract_id": "MAX_OUTPUT_HEADROOM", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/_token_budget.py", - "start_line": 79, - "end_line": 83, + "start_line": 73, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -48974,30 +51162,39 @@ "body": "# #region MAX_OUTPUT_HEADROOM [TYPE Constant]\n\n# Increased from 1000 to 3000 because SQL/dashboard text output varies significantly.\nMAX_OUTPUT_HEADROOM = 3000\n# #endregion MAX_OUTPUT_HEADROOM\n" }, { - "contract_id": "DictionaryUtils", + "contract_id": "TranslationUtils", "contract_type": "Module", "file_path": "backend/src/plugins/translate/_utils.py", "start_line": 1, - "end_line": 33, - "tier": "TIER_1", - "complexity": 1, + "end_line": 206, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "COMPLEXITY": 1, - "LAYER": "Domain", - "PURPOSE": "Utility helpers for dictionary management (term normalization and delimiter detection)." + "COMPLEXITY": 3, + "PURPOSE": "Shared utility functions for the translation plugin — dictionary enforcement," }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryUtils [C:1] [TYPE Module] [SEMANTICS translate, term, normalize, delimiter, csv]\n# @BRIEF Utility helpers for dictionary management (term normalization and delimiter detection).\n# @LAYER: Domain\n\nimport unicodedata\n\n\n# #region _normalize_term [TYPE Function]\n# @BRIEF Normalize a term for case-insensitive unique constraint lookup.\n# @RATIONALE: NFC normalization is applied before lowercasing to ensure consistent\n# comparison of Unicode characters (e.g. precomposed vs decomposed forms).\n# @REJECTED: Lowercasing without NFC normalization — would cause duplicate entries\n# for semantically identical Unicode strings in different normalization forms.\ndef _normalize_term(term: str) -> str:\n \"\"\"Normalize a term by NFC, lowercasing, and removing extra whitespace.\"\"\"\n if not term:\n return \"\"\n term = unicodedata.normalize('NFC', term)\n return \" \".join(term.lower().split())\n# #endregion _normalize_term\n\n\n# #region _detect_delimiter [TYPE Function]\n# @BRIEF Detect the delimiter used in a CSV/TSV header line.\ndef _detect_delimiter(header_line: str) -> str:\n \"\"\"Detect delimiter by counting tabs vs commas in the first line.\"\"\"\n if not header_line:\n return \",\"\n tab_count = header_line.count(\"\\t\")\n comma_count = header_line.count(\",\")\n return \"\\t\" if tab_count > comma_count else \",\"\n# #endregion _detect_delimiter\n# #endregion DictionaryUtils\n" + "body": "# #region TranslationUtils [C:3] [TYPE Module] [SEMANTICS translate, utils, hash, dictionary, cache]\n# @BRIEF Shared utility functions for the translation plugin — dictionary enforcement,\n# source hashing, cache lookup. Extracted from executor.py to break circular imports.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RATIONALE Extracted from TranslationExecutor to avoid circular imports when sub-services\n# (batch_proc, llm_call) need these helpers. The original monolithic executor.py\n# violated INV_7 at 1974 lines.\n\nimport hashlib\nimport json\nimport re\nimport unicodedata\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationLanguage, TranslationRecord\n\n\n# #region _normalize_term [TYPE Function]\n# @BRIEF Normalize a term for case-insensitive unique constraint lookup.\n# @RATIONALE: NFC normalization is applied before lowercasing to ensure consistent\n# comparison of Unicode characters (e.g. precomposed vs decomposed forms).\n# @REJECTED: Lowercasing without NFC normalization — would cause duplicate entries\n# for semantically identical Unicode strings in different normalization forms.\ndef _normalize_term(term: str) -> str:\n \"\"\"Normalize a term by NFC, lowercasing, and removing extra whitespace.\"\"\"\n if not term:\n return \"\"\n term = unicodedata.normalize('NFC', term)\n return \" \".join(term.lower().split())\n# #endregion _normalize_term\n\n\n# #region _detect_delimiter [TYPE Function]\n# @BRIEF Detect the delimiter used in a CSV/TSV header line.\ndef _detect_delimiter(header_line: str) -> str:\n \"\"\"Detect delimiter by counting tabs vs commas in the first line.\"\"\"\n if not header_line:\n return \",\"\n tab_count = header_line.count(\"\\t\")\n comma_count = header_line.count(\",\")\n return \"\\t\" if tab_count > comma_count else \",\"\n# #endregion _detect_delimiter\n\n\n# #region _enforce_dictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionary, post-processing]\n# @BRIEF Post-process LLM output: enforce dictionary term replacements.\n# @PRE dict_matches is a list of dict entries with source_term/target_term.\n# @POST per_lang_values may be mutated to include forced dictionary replacements.\n# @SIDE_EFFECT Logs when enforcement is applied.\ndef _enforce_dictionary(\n source_text: str,\n per_lang_values: dict[str, str],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n row_id: str,\n) -> None:\n \"\"\"Post-process LLM output: enforce dictionary term replacements.\n\n For each dictionary entry whose source_term appears in the source_text,\n verify that the target_term is present in each language's translation.\n If missing, replace any occurrence of the source term (which the LLM\n may have left untranslated) with the dictionary target term.\n \"\"\"\n if not dict_matches or not source_text:\n return\n\n text_lower = source_text.lower()\n\n for dm in dict_matches:\n src_term = dm.get(\"source_term\", \"\")\n tgt_term = dm.get(\"target_term\", \"\")\n if not src_term or not tgt_term:\n continue\n\n if src_term.lower() not in text_lower:\n continue\n\n for lang_code in list(per_lang_values.keys()):\n val = per_lang_values[lang_code]\n if not val:\n continue\n\n if tgt_term.lower() in val.lower():\n continue\n\n src_pattern = re.compile(re.escape(src_term), re.IGNORECASE)\n if src_pattern.search(val):\n new_val = src_pattern.sub(lambda _: tgt_term, val)\n if new_val != val:\n logger.reason(\"Dictionary enforcement applied\", {\n \"batch_id\": batch_id,\n \"row_id\": row_id,\n \"language_code\": lang_code,\n \"source_term\": src_term,\n \"target_term\": tgt_term,\n \"before\": val[:200],\n \"after\": new_val[:200],\n })\n per_lang_values[lang_code] = new_val\n# #endregion _enforce_dictionary\n\n\n# #region _compute_source_hash [C:2] [TYPE Function] [SEMANTICS translate, hash, cache-key]\n# @BRIEF Compute deterministic cache key for a source row.\n# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash).\ndef _compute_source_hash(\n source_text: str,\n source_data: dict | None,\n dict_snapshot_hash: str | None,\n config_hash: str | None,\n context_keys: list[str] | None = None,\n) -> str:\n \"\"\"Deterministic cache key for a translation source row.\n\n Only includes source_text and context-relevant fields from source_data.\n Key/identifier columns are excluded so identical text in different rows hits cache.\n \"\"\"\n context_data: dict[str, str] = {}\n if source_data and context_keys:\n for key in context_keys:\n val = source_data.get(key)\n if val is not None:\n context_data[key] = str(val)\n\n payload = json.dumps({\n \"text\": source_text,\n \"ctx\": context_data,\n \"dict_hash\": dict_snapshot_hash or \"\",\n \"config_hash\": config_hash or \"\",\n }, sort_keys=True, ensure_ascii=False)\n return hashlib.sha256(payload.encode(\"utf-8\")).hexdigest()\n# #endregion _compute_source_hash\n\n\n# #region _check_translation_cache [C:2] [TYPE Function] [SEMANTICS translate, cache, lookup]\n# @BRIEF Look up a previously successful translation by source_hash.\n# Returns per-language dict {lang_code: final_value} or None.\ndef _check_translation_cache(\n db: Session,\n source_hash: str,\n) -> dict[str, str] | None:\n \"\"\"Check if this source_hash was already translated successfully.\"\"\"\n cached = (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.source_hash == source_hash,\n TranslationRecord.status == \"SUCCESS\",\n )\n .order_by(TranslationRecord.created_at.desc())\n .first()\n )\n if not cached:\n return None\n\n lang_values: dict[str, str] = {}\n for lang in cached.languages:\n if lang.status == \"translated\" and lang.final_value:\n lang_values[lang.language_code] = lang.final_value\n\n return lang_values if lang_values else None\n# #endregion _check_translation_cache\n\n\n# #region _compute_key_hash [C:1] [TYPE Function]\n# @BRIEF Compute a stable hash from source_data dict for matching preview edits.\ndef _compute_key_hash(source_data: dict) -> str:\n \"\"\"Compute a stable hash from source_data dict for matching preview edits.\"\"\"\n import hashlib\n stable = json.dumps(source_data, sort_keys=True)\n return hashlib.sha256(stable.encode()).hexdigest()[:16]\n# #endregion _compute_key_hash\n\n\n# #region estimate_row_tokens [C:2] [TYPE Function] [SEMANTICS translate, token, estimation]\n# @BRIEF Estimate token count for a single source row including context fields.\n# @PRE source_text is a string.\n# @POST Returns estimated token count >= 1.\ndef estimate_row_tokens(\n source_text: str,\n source_data: dict | None,\n job,\n) -> int:\n \"\"\"Estimate token count for a single source row including context fields.\n\n Uses CJK-aware heuristics via _token_budget._estimate_tokens_for_text.\n Context fields from job.context_columns are included in the estimate.\n \"\"\"\n from ._token_budget import _estimate_tokens_for_text\n\n text_tokens = _estimate_tokens_for_text(source_text or \"\")\n\n context_keys = job.context_columns or []\n ctx_text = \"\"\n if source_data and context_keys:\n ctx_text = \" \".join(str(source_data.get(k, \"\")) for k in context_keys)\n ctx_tokens = _estimate_tokens_for_text(ctx_text)\n\n return text_tokens + ctx_tokens\n# #endregion estimate_row_tokens\n# #endregion TranslationUtils\n" }, { "contract_id": "_normalize_term", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_utils.py", - "start_line": 8, - "end_line": 20, + "start_line": 23, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49014,8 +51211,8 @@ "contract_id": "_detect_delimiter", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_utils.py", - "start_line": 23, - "end_line": 32, + "start_line": 38, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49027,33 +51224,420 @@ "tier_source": "AutoCalculated", "body": "# #region _detect_delimiter [TYPE Function]\n# @BRIEF Detect the delimiter used in a CSV/TSV header line.\ndef _detect_delimiter(header_line: str) -> str:\n \"\"\"Detect delimiter by counting tabs vs commas in the first line.\"\"\"\n if not header_line:\n return \",\"\n tab_count = header_line.count(\"\\t\")\n comma_count = header_line.count(\",\")\n return \"\\t\" if tab_count > comma_count else \",\"\n# #endregion _detect_delimiter\n" }, + { + "contract_id": "_enforce_dictionary", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_utils.py", + "start_line": 50, + "end_line": 105, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "per_lang_values may be mutated to include forced dictionary replacements.", + "PRE": "dict_matches is a list of dict entries with source_term/target_term.", + "PURPOSE": "Post-process LLM output: enforce dictionary term replacements.", + "SIDE_EFFECT": "Logs when enforcement is applied." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _enforce_dictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionary, post-processing]\n# @BRIEF Post-process LLM output: enforce dictionary term replacements.\n# @PRE dict_matches is a list of dict entries with source_term/target_term.\n# @POST per_lang_values may be mutated to include forced dictionary replacements.\n# @SIDE_EFFECT Logs when enforcement is applied.\ndef _enforce_dictionary(\n source_text: str,\n per_lang_values: dict[str, str],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n row_id: str,\n) -> None:\n \"\"\"Post-process LLM output: enforce dictionary term replacements.\n\n For each dictionary entry whose source_term appears in the source_text,\n verify that the target_term is present in each language's translation.\n If missing, replace any occurrence of the source term (which the LLM\n may have left untranslated) with the dictionary target term.\n \"\"\"\n if not dict_matches or not source_text:\n return\n\n text_lower = source_text.lower()\n\n for dm in dict_matches:\n src_term = dm.get(\"source_term\", \"\")\n tgt_term = dm.get(\"target_term\", \"\")\n if not src_term or not tgt_term:\n continue\n\n if src_term.lower() not in text_lower:\n continue\n\n for lang_code in list(per_lang_values.keys()):\n val = per_lang_values[lang_code]\n if not val:\n continue\n\n if tgt_term.lower() in val.lower():\n continue\n\n src_pattern = re.compile(re.escape(src_term), re.IGNORECASE)\n if src_pattern.search(val):\n new_val = src_pattern.sub(lambda _: tgt_term, val)\n if new_val != val:\n logger.reason(\"Dictionary enforcement applied\", {\n \"batch_id\": batch_id,\n \"row_id\": row_id,\n \"language_code\": lang_code,\n \"source_term\": src_term,\n \"target_term\": tgt_term,\n \"before\": val[:200],\n \"after\": new_val[:200],\n })\n per_lang_values[lang_code] = new_val\n# #endregion _enforce_dictionary\n" + }, + { + "contract_id": "_compute_source_hash", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_utils.py", + "start_line": 108, + "end_line": 137, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Compute deterministic cache key for a source row." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _compute_source_hash [C:2] [TYPE Function] [SEMANTICS translate, hash, cache-key]\n# @BRIEF Compute deterministic cache key for a source row.\n# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash).\ndef _compute_source_hash(\n source_text: str,\n source_data: dict | None,\n dict_snapshot_hash: str | None,\n config_hash: str | None,\n context_keys: list[str] | None = None,\n) -> str:\n \"\"\"Deterministic cache key for a translation source row.\n\n Only includes source_text and context-relevant fields from source_data.\n Key/identifier columns are excluded so identical text in different rows hits cache.\n \"\"\"\n context_data: dict[str, str] = {}\n if source_data and context_keys:\n for key in context_keys:\n val = source_data.get(key)\n if val is not None:\n context_data[key] = str(val)\n\n payload = json.dumps({\n \"text\": source_text,\n \"ctx\": context_data,\n \"dict_hash\": dict_snapshot_hash or \"\",\n \"config_hash\": config_hash or \"\",\n }, sort_keys=True, ensure_ascii=False)\n return hashlib.sha256(payload.encode(\"utf-8\")).hexdigest()\n# #endregion _compute_source_hash\n" + }, + { + "contract_id": "_check_translation_cache", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_utils.py", + "start_line": 140, + "end_line": 167, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Look up a previously successful translation by source_hash." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _check_translation_cache [C:2] [TYPE Function] [SEMANTICS translate, cache, lookup]\n# @BRIEF Look up a previously successful translation by source_hash.\n# Returns per-language dict {lang_code: final_value} or None.\ndef _check_translation_cache(\n db: Session,\n source_hash: str,\n) -> dict[str, str] | None:\n \"\"\"Check if this source_hash was already translated successfully.\"\"\"\n cached = (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.source_hash == source_hash,\n TranslationRecord.status == \"SUCCESS\",\n )\n .order_by(TranslationRecord.created_at.desc())\n .first()\n )\n if not cached:\n return None\n\n lang_values: dict[str, str] = {}\n for lang in cached.languages:\n if lang.status == \"translated\" and lang.final_value:\n lang_values[lang.language_code] = lang.final_value\n\n return lang_values if lang_values else None\n# #endregion _check_translation_cache\n" + }, + { + "contract_id": "_compute_key_hash", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_utils.py", + "start_line": 170, + "end_line": 177, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Compute a stable hash from source_data dict for matching preview edits." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _compute_key_hash [C:1] [TYPE Function]\n# @BRIEF Compute a stable hash from source_data dict for matching preview edits.\ndef _compute_key_hash(source_data: dict) -> str:\n \"\"\"Compute a stable hash from source_data dict for matching preview edits.\"\"\"\n import hashlib\n stable = json.dumps(source_data, sort_keys=True)\n return hashlib.sha256(stable.encode()).hexdigest()[:16]\n# #endregion _compute_key_hash\n" + }, + { + "contract_id": "estimate_row_tokens", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/_utils.py", + "start_line": 180, + "end_line": 205, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Returns estimated token count >= 1.", + "PRE": "source_text is a string.", + "PURPOSE": "Estimate token count for a single source row including context fields." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region estimate_row_tokens [C:2] [TYPE Function] [SEMANTICS translate, token, estimation]\n# @BRIEF Estimate token count for a single source row including context fields.\n# @PRE source_text is a string.\n# @POST Returns estimated token count >= 1.\ndef estimate_row_tokens(\n source_text: str,\n source_data: dict | None,\n job,\n) -> int:\n \"\"\"Estimate token count for a single source row including context fields.\n\n Uses CJK-aware heuristics via _token_budget._estimate_tokens_for_text.\n Context fields from job.context_columns are included in the estimate.\n \"\"\"\n from ._token_budget import _estimate_tokens_for_text\n\n text_tokens = _estimate_tokens_for_text(source_text or \"\")\n\n context_keys = job.context_columns or []\n ctx_text = \"\"\n if source_data and context_keys:\n ctx_text = \" \".join(str(source_data.get(k, \"\")) for k in context_keys)\n ctx_tokens = _estimate_tokens_for_text(ctx_text)\n\n return text_tokens + ctx_tokens\n# #endregion estimate_row_tokens\n" + }, + { + "contract_id": "DictionaryManagerModule", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/dictionary.py", + "start_line": 1, + "end_line": 68, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "LAYER": "Domain", + "POST": "Dictionary and entry mutations are persisted with conflict detection.", + "PRE": "Database session is open and valid.", + "PURPOSE": "Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.", + "RATIONALE": "C4 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion.", + "REJECTED": "Monolithic DictionaryManager class — violated INV_7. Decomposed into DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.", + "SIDE_EFFECT": "Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards." + }, + "relations": [ + { + "source_id": "DictionaryManagerModule", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob:Class", + "target_ref": "[TranslationJob:Class]" + }, + { + "source_id": "DictionaryManagerModule", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryEntry:Class", + "target_ref": "[DictionaryEntry:Class]" + }, + { + "source_id": "DictionaryManagerModule", + "relation_type": "DEPENDS_ON", + "target_id": "TerminologyDictionary:Class", + "target_ref": "[TerminologyDictionary:Class]" + }, + { + "source_id": "DictionaryManagerModule", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJobDictionary:Class", + "target_ref": "[TranslationJobDictionary:Class]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region DictionaryManagerModule [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, dictionary, batch, term]\n# @BRIEF Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob:Class]\n# @RELATION DEPENDS_ON -> [DictionaryEntry:Class]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary:Class]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary:Class]\n# @RATIONALE C4 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion.\n# @REJECTED \"Keep both\" as conflict option — UniqueConstraint prohibits variants; only overwrite/keep existing.\n# @REJECTED Monolithic DictionaryManager class — violated INV_7. Decomposed into DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @PRE Database session is open and valid.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom .dictionary_correction import DictionaryCorrectionService\nfrom .dictionary_crud import DictionaryCRUD\nfrom .dictionary_entries import DictionaryEntryCRUD\nfrom .dictionary_filter import DictionaryBatchFilter\nfrom .dictionary_import_export import DictionaryImportExport\nfrom .dictionary_validation import _validate_bcp47\n\n\n# #region DictionaryManager [C:4] [TYPE Class]\n# @BRIEF Facade for terminology dictionaries: delegates to DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @PRE Database session is open and valid.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n# @RATIONALE Thin facade preserves API compatibility after decomposition.\n# @REJECTED Removing the facade would break all upstream imports; delegation pattern preferred.\nclass DictionaryManager:\n \"\"\"Facade for terminology dictionary operations — delegates to specialized sub-classes.\"\"\"\n\n # Dictionary CRUD\n create_dictionary = DictionaryCRUD.create_dictionary\n update_dictionary = DictionaryCRUD.update_dictionary\n delete_dictionary = DictionaryCRUD.delete_dictionary\n get_dictionary = DictionaryCRUD.get_dictionary\n list_dictionaries = DictionaryCRUD.list_dictionaries\n\n # Entry CRUD\n add_entry = DictionaryEntryCRUD.add_entry\n edit_entry = DictionaryEntryCRUD.edit_entry\n delete_entry = DictionaryEntryCRUD.delete_entry\n clear_entries = DictionaryEntryCRUD.clear_entries\n list_entries = DictionaryEntryCRUD.list_entries\n\n # Import/Export\n import_entries = DictionaryImportExport.import_entries\n export_entries = DictionaryImportExport.export_entries\n migrate_old_entries = DictionaryImportExport.migrate_old_entries\n\n # Batch filtering\n filter_for_batch = DictionaryBatchFilter.filter_for_batch\n\n # Corrections\n submit_correction = DictionaryCorrectionService.submit_correction\n submit_bulk_corrections = DictionaryCorrectionService.submit_bulk_corrections\n\n# #endregion DictionaryManager\n\n\n# #endregion DictionaryManagerModule\n" + }, { "contract_id": "DictionaryManager", "contract_type": "Class", "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 48, - "end_line": 1003, + "start_line": 29, + "end_line": 65, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, "POST": "Dictionary and entry mutations are persisted with conflict detection.", "PRE": "Database session is open and valid.", - "PURPOSE": "Manages terminology dictionaries and their entries with referential integrity.", + "PURPOSE": "Facade for terminology dictionaries: delegates to DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.", + "RATIONALE": "Thin facade preserves API compatibility after decomposition.", + "REJECTED": "Removing the facade would break all upstream imports; delegation pattern preferred.", "SIDE_EFFECT": "Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards." }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region DictionaryManager [C:4] [TYPE Class]\n# @BRIEF Facade for terminology dictionaries: delegates to DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @PRE Database session is open and valid.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n# @RATIONALE Thin facade preserves API compatibility after decomposition.\n# @REJECTED Removing the facade would break all upstream imports; delegation pattern preferred.\nclass DictionaryManager:\n \"\"\"Facade for terminology dictionary operations — delegates to specialized sub-classes.\"\"\"\n\n # Dictionary CRUD\n create_dictionary = DictionaryCRUD.create_dictionary\n update_dictionary = DictionaryCRUD.update_dictionary\n delete_dictionary = DictionaryCRUD.delete_dictionary\n get_dictionary = DictionaryCRUD.get_dictionary\n list_dictionaries = DictionaryCRUD.list_dictionaries\n\n # Entry CRUD\n add_entry = DictionaryEntryCRUD.add_entry\n edit_entry = DictionaryEntryCRUD.edit_entry\n delete_entry = DictionaryEntryCRUD.delete_entry\n clear_entries = DictionaryEntryCRUD.clear_entries\n list_entries = DictionaryEntryCRUD.list_entries\n\n # Import/Export\n import_entries = DictionaryImportExport.import_entries\n export_entries = DictionaryImportExport.export_entries\n migrate_old_entries = DictionaryImportExport.migrate_old_entries\n\n # Batch filtering\n filter_for_batch = DictionaryBatchFilter.filter_for_batch\n\n # Corrections\n submit_correction = DictionaryCorrectionService.submit_correction\n submit_bulk_corrections = DictionaryCorrectionService.submit_bulk_corrections\n\n# #endregion DictionaryManager\n" + }, + { + "contract_id": "DictionaryCorrectionService", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/dictionary_correction.py", + "start_line": 1, + "end_line": 192, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Submit term corrections and bulk corrections to dictionaries with conflict detection." + }, "relations": [ { - "source_id": "DictionaryManager", + "source_id": "DictionaryCorrectionService", "relation_type": "DEPENDS_ON", - "target_id": "TerminologyDictionary:Class], DEPENDS_ON -> [DictionaryEntry:Class], DEPENDS_ON -> [TranslationJobDictionary:Class], DEPENDS_ON -> [TranslationJob:Class", - "target_ref": "[TerminologyDictionary:Class], DEPENDS_ON -> [DictionaryEntry:Class], DEPENDS_ON -> [TranslationJobDictionary:Class], DEPENDS_ON -> [TranslationJob:Class]" + "target_id": "DictionaryEntry", + "target_ref": "[DictionaryEntry]" + }, + { + "source_id": "DictionaryCorrectionService", + "relation_type": "DEPENDS_ON", + "target_id": "TerminologyDictionary", + "target_ref": "[TerminologyDictionary]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryManager [C:4] [TYPE Class]\n# @BRIEF Manages terminology dictionaries and their entries with referential integrity.\n# @PRE Database session is open and valid.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n# @RELATION DEPENDS_ON -> [TerminologyDictionary:Class], DEPENDS_ON -> [DictionaryEntry:Class], DEPENDS_ON -> [TranslationJobDictionary:Class], DEPENDS_ON -> [TranslationJob:Class]\n\nclass DictionaryManager:\n # region DictionaryManager.create_dictionary [TYPE Function]\n # @PURPOSE Create a new terminology dictionary (language independent at dictionary level; language pairs are per-entry).\n # @PRE name is provided.\n # @POST New TerminologyDictionary row is created and returned.\n @staticmethod\n def create_dictionary(\n db: Session, name: str,\n source_dialect: str = \"\",\n target_dialect: str = \"\",\n created_by: str | None = None, description: str | None = None,\n is_active: bool = True,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.create_dictionary\"):\n logger.reason(\"Creating dictionary\", {\"name\": name, \"source\": source_dialect, \"target\": target_dialect})\n dictionary = TerminologyDictionary(\n name=name,\n description=description,\n source_dialect=source_dialect or \"\",\n target_dialect=target_dialect or \"\",\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n logger.reflect(\"Dictionary created\", {\"id\": dictionary.id})\n return dictionary\n # endregion DictionaryManager.create_dictionary\n\n # region DictionaryManager.update_dictionary [TYPE Function]\n # @PURPOSE Update an existing terminology dictionary.\n # @PRE dict_id exists in terminology_dictionaries table.\n # @POST Dictionary metadata is updated and returned.\n @staticmethod\n def update_dictionary(\n db: Session, dict_id: str, name: str | None = None,\n description: str | None = None, source_dialect: str | None = None,\n target_dialect: str | None = None, is_active: bool | None = None,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.update_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n logger.reason(\"Updating dictionary\", {\"id\": dict_id})\n if name is not None:\n dictionary.name = name\n if description is not None:\n dictionary.description = description\n if source_dialect is not None:\n dictionary.source_dialect = source_dialect\n if target_dialect is not None:\n dictionary.target_dialect = target_dialect\n if is_active is not None:\n dictionary.is_active = is_active\n db.commit()\n db.refresh(dictionary)\n logger.reflect(\"Dictionary updated\", {\"id\": dictionary.id})\n return dictionary\n # endregion DictionaryManager.update_dictionary\n\n # region DictionaryManager.delete_dictionary [TYPE Function]\n # @PURPOSE Delete a dictionary, blocked if attached to active/scheduled jobs.\n # @PRE dict_id exists.\n # @POST Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs.\n # @SIDE_EFFECT Deletes TerminologyDictionary row and all DictionaryEntry rows.\n @staticmethod\n def delete_dictionary(db: Session, dict_id: str) -> None:\n with belief_scope(\"DictionaryManager.delete_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n # Check for attached active/scheduled jobs\n blocked_statuses = [\"ACTIVE\", \"READY\", \"RUNNING\", \"SCHEDULED\"]\n attached_jobs = (\n db.query(TranslationJobDictionary)\n .filter(\n TranslationJobDictionary.dictionary_id == dict_id,\n TranslationJobDictionary.job_id.in_(\n db.query(TranslationJob.id).filter(\n TranslationJob.status.in_(blocked_statuses)\n )\n ),\n )\n .count()\n )\n\n if attached_jobs > 0:\n logger.explore(\"Delete blocked: dictionary attached to active jobs\", {\"dict_id\": dict_id, \"jobs\": attached_jobs})\n raise ValueError(\n f\"Cannot delete dictionary attached to {attached_jobs} active/scheduled job(s). \"\n \"Remove dictionary associations from jobs first.\"\n )\n\n logger.reason(\"Deleting dictionary\", {\"id\": dict_id})\n # Delete entries and job-dictionary links first\n db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.query(TranslationJobDictionary).filter(TranslationJobDictionary.dictionary_id == dict_id).delete()\n db.delete(dictionary)\n db.commit()\n logger.reflect(\"Dictionary deleted\", {\"id\": dict_id})\n # endregion DictionaryManager.delete_dictionary\n\n # region DictionaryManager.get_dictionary [TYPE Function]\n # @PURPOSE Get a single dictionary by ID with entry count.\n # @PRE dict_id exists.\n # @POST Returns dict with dictionary + entry_count or raises ValueError.\n @staticmethod\n def get_dictionary(db: Session, dict_id: str) -> TerminologyDictionary:\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n return dictionary\n # endregion DictionaryManager.get_dictionary\n\n # region DictionaryManager.list_dictionaries [TYPE Function]\n # @PURPOSE List dictionaries with pagination and entry counts.\n # @PRE page >= 1, page_size between 1 and 100.\n # @POST Returns (list of dicts, total_count).\n @staticmethod\n def list_dictionaries(\n db: Session, page: int = 1, page_size: int = 20,\n ) -> tuple[list[TerminologyDictionary], int]:\n total = db.query(func.count(TerminologyDictionary.id)).scalar() or 0\n dictionaries = (\n db.query(TerminologyDictionary)\n .order_by(TerminologyDictionary.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return dictionaries, total\n # endregion DictionaryManager.list_dictionaries\n\n # region DictionaryManager.add_entry [TYPE Function]\n # @PURPOSE Add an entry to a dictionary with language-pair-aware uniqueness validation.\n # @PRE dict_id exists. source_term, target_term are non-empty. source_language, target_language are valid BCP-47.\n # @POST New DictionaryEntry row is created or raises on duplicate.\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\",\n target_language: str = \"und\",\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n _validate_bcp47(source_language, \"source_language\")\n _validate_bcp47(target_language, \"target_language\")\n\n normalized = _normalize_term(source_term)\n # Uniqueness is now (dictionary_id, source_term_normalized, source_language, target_language)\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, \"src_lang\": source_language, \"tgt_lang\": target_language})\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, \"src_lang\": source_language, \"tgt_lang\": target_language})\n return entry\n # endregion DictionaryManager.add_entry\n\n # region DictionaryManager.edit_entry [TYPE Function]\n # @PURPOSE Edit an existing dictionary entry with language-pair-aware duplicate check.\n # @PRE entry_id exists.\n # @POST Entry fields are updated.\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,\n target_language: str | None = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n\n 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 # Check uniqueness within the same dictionary + language pair\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(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n logger.reflect(\"Entry updated\", {\"entry_id\": entry.id})\n return entry\n # endregion DictionaryManager.edit_entry\n\n # region DictionaryManager.delete_entry [TYPE Function]\n # @PURPOSE Delete a single dictionary entry.\n # @PRE entry_id exists.\n # @POST Entry is deleted.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryManager.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n 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 DictionaryManager.delete_entry\n\n # region DictionaryManager.clear_entries [TYPE Function]\n # @PURPOSE Delete all entries for a dictionary.\n # @PRE dict_id exists.\n # @POST All entries for the dictionary are deleted.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryManager.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n 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 DictionaryManager.clear_entries\n\n # region DictionaryManager.list_entries [TYPE Function]\n # @PURPOSE List entries for a dictionary with pagination.\n # @PRE dict_id exists.\n # @POST Returns (list of entries, total_count).\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> tuple[list[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar()\n or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # endregion DictionaryManager.list_entries\n\n # region DictionaryManager.import_entries [TYPE Function]\n # @PURPOSE Import entries from CSV/TSV content with duplicate detection and conflict resolution.\n # @PRE content is valid CSV or TSV. dict_id exists.\n # @POST Entries are created/updated/skipped per conflict mode. Returns result summary.\n # @SIDE_EFFECT Batch-inserts or updates DictionaryEntry rows.\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(\"DictionaryManager.import_entries\"):\n # Validate dictionary\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n # Detect delimiter if not specified\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 # Parse content\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] = {\n \"total\": 0,\n \"created\": 0,\n \"updated\": 0,\n \"skipped\": 0,\n \"errors\": [],\n \"preview\": [],\n }\n\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({\n \"line\": row_idx + 2, # +2 for header and 1-indexed\n \"error\": \"Both source_term and target_term are required\",\n \"row\": row,\n })\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 preview_row = {\n \"line\": row_idx + 2,\n \"source_term\": source_term,\n \"target_term\": target_term,\n \"context_notes\": context_notes,\n \"source_language\": source_language,\n \"target_language\": target_language,\n \"is_conflict\": existing is not None,\n \"existing_target_term\": existing.target_term if existing else None,\n }\n result[\"preview\"].append(preview_row)\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: # cancel\n result[\"errors\"].append({\n \"line\": row_idx + 2,\n \"error\": f\"Conflict on '{source_term}' and on_conflict='cancel'\",\n \"row\": row,\n })\n continue\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term,\n source_term_normalized=normalized,\n target_term=target_term,\n context_notes=context_notes,\n source_language=source_language,\n target_language=target_language,\n )\n db.add(entry)\n result[\"created\"] += 1\n\n except Exception as e:\n result[\"errors\"].append({\n \"line\": row_idx + 2,\n \"error\": str(e),\n \"row\": row,\n })\n\n if not preview_only:\n db.commit()\n\n logger.reflect(\"Import complete\", {\n \"dict_id\": dict_id,\n \"total\": result[\"total\"],\n \"created\": result[\"created\"],\n \"updated\": result[\"updated\"],\n \"skipped\": result[\"skipped\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n # endregion DictionaryManager.import_entries\n\n # region DictionaryManager.export_entries [TYPE Function]\n # @PURPOSE Export all entries as CSV string with language columns.\n # @PRE dict_id exists.\n # @POST Returns CSV string with header: source_term, target_term, source_language, target_language, context_notes, context_data, usage_notes.\n @staticmethod\n def export_entries(\n db: Session, dict_id: str, delimiter: str = \",\",\n ) -> str:\n with belief_scope(\"DictionaryManager.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 = [\n \"source_term\", \"target_term\",\n \"source_language\", \"target_language\",\n \"context_notes\", \"context_data\", \"usage_notes\",\n ]\n writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=delimiter)\n writer.writeheader()\n\n for entry in entries:\n writer.writerow({\n \"source_term\": entry.source_term,\n \"target_term\": entry.target_term,\n \"source_language\": entry.source_language,\n \"target_language\": entry.target_language,\n \"context_notes\": entry.context_notes or \"\",\n \"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), \"delimiter\": repr(delimiter)})\n return result\n # endregion DictionaryManager.export_entries\n\n # region DictionaryManager.migrate_old_entries [TYPE Function]\n # @PURPOSE Migrate existing single-language dictionary entries to populate source_language and target_language.\n # @PRE db session is open.\n # @POST Entries with \"und\" source_language or target_language are updated with inferred values.\n # @SIDE_EFFECT Updates DictionaryEntry rows in bulk.\n @staticmethod\n def migrate_old_entries(db: Session) -> dict[str, int]:\n with belief_scope(\"DictionaryManager.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 # Find all entries that may need migration (source_language is \"und\" or target_language is \"und\")\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 # Try to infer source_language from origin_source_language\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\n # Note: target_language inference from dictionary-level column was removed\n # because TerminologyDictionary.target_language was deprecated and dropped.\n\n if entry.source_language == \"und\" and entry.target_language == \"und\":\n skipped += 1\n\n db.commit()\n\n logger.reflect(\"Migration complete\", {\n \"migrated_source\": migrated_source,\n \"migrated_target\": migrated_target,\n \"skipped\": skipped,\n \"total_processed\": len(entries),\n })\n return {\n \"migrated_source\": migrated_source,\n \"migrated_target\": migrated_target,\n \"skipped\": skipped,\n \"total_processed\": len(entries),\n }\n # endregion DictionaryManager.migrate_old_entries\n\n # region DictionaryManager.filter_for_batch [TYPE Function]\n # @PURPOSE Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job,\n # optionally filtered by language pair. When row_context is provided, computes context-aware priority flags.\n # @PRE job_id exists and source_texts is a list of strings.\n # @POST Returns list of matched entries with match info, sorted by priority across attached dictionaries.\n # When row_context is given, each result includes a 'priority_match' boolean.\n # @SIDE_EFFECT Queries TranslationJobDictionary, TerminologyDictionary, and DictionaryEntry tables.\n @staticmethod\n def filter_for_batch(\n db: Session, source_texts: list[str], job_id: str,\n source_language: str | None = None,\n target_language: str | None = None,\n row_context: dict | None = None,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"DictionaryManager.filter_for_batch\"):\n # Get dictionaries attached to this job\n job_dict_links = (\n db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n if not job_dict_links:\n logger.reason(\"No dictionaries attached to job\", {\"job_id\": job_id})\n return []\n\n dict_ids = [jd.dictionary_id for jd in job_dict_links]\n\n # Get all active dictionaries\n dictionaries = (\n db.query(TerminologyDictionary)\n .filter(\n TerminologyDictionary.id.in_(dict_ids),\n TerminologyDictionary.is_active == True,\n )\n .all()\n )\n if not dictionaries:\n return []\n\n # Build dict_id -> dict_name lookup\n dict_map = {d.id: d.name for d in dictionaries}\n\n # Fetch all entries for these dictionaries, ordered by dictionary (priority order from link order)\n all_entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .order_by(DictionaryEntry.source_term.asc())\n .all()\n )\n\n if not all_entries:\n return []\n\n # PERFORMANCE NOTE: O(rows × entries) regex matching per batch.\n # Acceptable for current scale (50-100 rows × <1000 entries).\n # For larger batches (>500 rows or >5000 entries), consider\n # replacing with an Aho-Corasick automaton (e.g., pyahocorasick).\n matched: list[dict[str, Any]] = []\n seen_source_match: set = set()\n\n for text_idx, source_text in enumerate(source_texts):\n if not source_text:\n continue\n\n text_lower = source_text.lower()\n\n for entry in all_entries:\n norm = entry.source_term_normalized\n if not norm:\n continue\n\n # Apply language pair filtering\n if source_language is not None:\n src = source_language.strip().lower()\n entry_src = entry.source_language.strip().lower()\n if entry_src != src and entry_src != \"und\":\n continue\n if target_language is not None:\n tgt = target_language.strip().lower()\n entry_tgt = entry.target_language.strip().lower()\n if entry_tgt != tgt:\n continue\n\n # Word-boundary-aware matching\n # Build pattern: \\bterm\\b (case-insensitive)\n # Escape regex special chars in the search term\n escaped = re.escape(norm)\n pattern = re.compile(r\"\\b\" + escaped + r\"\\b\", re.IGNORECASE)\n\n if pattern.search(text_lower):\n match_key = (text_idx, entry.id)\n if match_key not in seen_source_match:\n seen_source_match.add(match_key)\n\n # Compute context-aware priority if row_context is available\n priority_match = False\n if row_context and entry.has_context and entry.context_data:\n similarity = ContextAwarePromptBuilder.compute_context_similarity(\n entry.context_data, row_context\n )\n priority_match = similarity >= 0.5\n\n matched.append({\n \"text_index\": text_idx,\n \"source_text\": source_text,\n \"entry_id\": entry.id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"dictionary_id\": entry.dictionary_id,\n \"dictionary_name\": dict_map.get(entry.dictionary_id, \"\"),\n \"context_notes\": entry.context_notes,\n \"context_data\": entry.context_data,\n \"has_context\": entry.has_context,\n \"context_source\": entry.context_source,\n \"usage_notes\": entry.usage_notes,\n \"source_language\": entry.source_language,\n \"target_language\": entry.target_language,\n \"priority_match\": priority_match,\n })\n\n # Sort by dictionary link priority (order of dict_ids from link order)\n dict_priority = {d_id: idx for idx, d_id in enumerate(dict_ids)}\n matched.sort(key=lambda m: (dict_priority.get(m[\"dictionary_id\"], 999), m[\"text_index\"]))\n\n logger.reflect(\"Batch filter match complete\", {\n \"job_id\": job_id,\n \"source_texts\": len(source_texts),\n \"matches\": len(matched),\n \"dictionaries_used\": len(dictionaries),\n \"source_language\": source_language,\n \"target_language\": target_language,\n })\n return matched\n # endregion DictionaryManager.filter_for_batch\n\n\n # region DictionaryManager.submit_correction [TYPE Function]\n # @PURPOSE Submit a term correction from a run result. Validates language match, detects conflicts.\n # @PRE source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists.\n # @POST Entry created or updated; origin tracking populated. Returns action + conflict info.\n # @SIDE_EFFECT Creates/updates DictionaryEntry row.\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(\"DictionaryManager.submit_correction\"):\n # Validate dictionary exists\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 # Handle keep_context=False (user explicitly removed context)\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 # Find existing entry: match exact language pair OR old-style \"und\"/\"und\" entries\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n or_(\n # Exact language pair match\n (DictionaryEntry.source_language == entry_src_lang) &\n (DictionaryEntry.target_language == entry_tgt_lang),\n # Old-style entries without language pair\n (DictionaryEntry.source_language == \"und\") &\n (DictionaryEntry.target_language == \"und\"),\n ),\n )\n .first()\n )\n\n result: dict[str, Any] = {\n \"source_term\": source_term,\n \"target_term\": corrected_target_term,\n \"action\": \"created\",\n \"entry_id\": None,\n \"conflict\": None,\n \"message\": None,\n }\n\n if existing:\n # Conflict detected\n if on_conflict == \"keep_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_target_term,\n \"action\": \"keep_existing\",\n }\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: # cancel\n result[\"action\"] = \"skipped\"\n result[\"conflict\"] = {\n \"source_term\": source_term,\n \"existing_target_term\": existing.target_term,\n \"submitted_target_term\": corrected_target_term,\n \"action\": \"cancel\",\n }\n result[\"message\"] = \"Correction cancelled by conflict mode\"\n return result\n else:\n # Create new entry — derive language pair from dictionary\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=corrected_target_term.strip(),\n source_language=entry_src_lang,\n target_language=entry_tgt_lang,\n context_data=effective_context,\n usage_notes=usage_notes,\n has_context=bool(effective_context),\n context_source=effective_context_source if effective_context else None,\n origin_run_id=origin_run_id,\n origin_row_key=origin_row_key,\n 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 DictionaryManager.submit_correction\n\n # region DictionaryManager.submit_bulk_corrections [TYPE Function]\n # @PURPOSE Submit multiple term corrections atomically — all succeed or all fail.\n # @PRE corrections list is non-empty. dict_id exists.\n # @POST All corrections applied or none applied with conflict list.\n # @SIDE_EFFECT Creates/updates DictionaryEntry rows; commits once.\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(\"DictionaryManager.submit_bulk_corrections\"):\n # Validate dictionary first\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 incorrect_target = corr.get(\"incorrect_target_term\", \"\").strip()\n corrected_target = corr.get(\"corrected_target_term\", \"\").strip()\n\n if not source_term or not corrected_target:\n results.append({\n \"source_term\": source_term,\n \"action\": \"error\",\n \"message\": \"source_term and corrected_target_term are required\",\n })\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 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) &\n (DictionaryEntry.target_language == bulk_tgt_lang),\n (DictionaryEntry.source_language == \"und\") &\n (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({\n \"source_term\": source_term,\n \"existing_target_term\": existing.target_term,\n \"submitted_target_term\": corrected_target,\n \"action\": \"keep_existing\",\n })\n results.append({\n \"source_term\": source_term,\n \"action\": \"conflict_detected\",\n \"message\": f\"Existing entry kept: '{existing.target_term}'\",\n })\n continue\n elif on_conflict == \"overwrite\":\n existing.target_term = corrected_target\n existing.origin_user_id = origin_user_id\n results.append({\n \"source_term\": source_term,\n \"action\": \"updated\",\n \"entry_id\": existing.id,\n \"message\": f\"Updated to '{corrected_target}'\",\n })\n else:\n conflicts.append({\n \"source_term\": source_term,\n \"existing_target_term\": existing.target_term,\n \"submitted_target_term\": corrected_target,\n \"action\": \"cancel\",\n })\n results.append({\n \"source_term\": source_term,\n \"action\": \"skipped\",\n \"message\": \"Cancelled by conflict mode\",\n })\n else:\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term,\n source_term_normalized=normalized,\n target_term=corrected_target,\n source_language=bulk_src_lang,\n target_language=bulk_tgt_lang,\n origin_run_id=corr.get(\"origin_run_id\"),\n origin_row_key=corr.get(\"origin_row_key\"),\n origin_user_id=origin_user_id,\n )\n db.add(entry)\n results.append({\n \"source_term\": source_term,\n \"action\": \"created\",\n \"message\": f\"Entry created for '{source_term}' -> '{corrected_target}'\",\n })\n\n if conflicts and not all_ok:\n # Rollback — all failed\n db.rollback()\n return {\n \"status\": \"conflicts\",\n \"results\": results,\n \"conflicts\": conflicts,\n \"message\": \"Conflicts detected. Resolve before retrying.\",\n }\n\n db.commit()\n\n return {\n \"status\": \"completed\",\n \"results\": results,\n \"conflicts\": conflicts,\n \"total\": len(corrections),\n \"applied\": sum(1 for r in results if r[\"action\"] in (\"created\", \"updated\")),\n }\n # endregion DictionaryManager.submit_bulk_corrections\n\n\n# #endregion DictionaryManager\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", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/dictionary_crud.py", + "start_line": 1, + "end_line": 136, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "CRUD operations for TerminologyDictionary records." + }, + "relations": [ + { + "source_id": "DictionaryCRUD", + "relation_type": "DEPENDS_ON", + "target_id": "TerminologyDictionary", + "target_ref": "[TerminologyDictionary]" + }, + { + "source_id": "DictionaryCRUD", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "DictionaryCRUD", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJobDictionary", + "target_ref": "[TranslationJobDictionary]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region DictionaryCRUD [C:3] [TYPE Class] [SEMANTICS dictionary, crud, terminology]\n# @BRIEF CRUD operations for TerminologyDictionary records.\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n DictionaryEntry,\n TerminologyDictionary,\n TranslationJob,\n TranslationJobDictionary,\n)\n\n\nclass DictionaryCRUD:\n \"\"\"CRUD operations for terminology dictionaries.\"\"\"\n\n # region create_dictionary [TYPE Function]\n # @PURPOSE: Create a new terminology dictionary.\n @staticmethod\n def create_dictionary(\n db: Session, name: str,\n source_dialect: str = \"\", target_dialect: str = \"\",\n created_by: str | None = None, description: str | None = None,\n is_active: bool = True,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryCRUD.create_dictionary\"):\n logger.reason(\"Creating dictionary\", {\"name\": name, \"source\": source_dialect, \"target\": target_dialect})\n dictionary = TerminologyDictionary(\n name=name, description=description,\n source_dialect=source_dialect or \"\", target_dialect=target_dialect or \"\",\n is_active=is_active, created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n logger.reflect(\"Dictionary created\", {\"id\": dictionary.id})\n return dictionary\n # endregion create_dictionary\n\n # region update_dictionary [TYPE Function]\n # @PURPOSE: Update an existing terminology dictionary.\n @staticmethod\n def update_dictionary(\n db: Session, dict_id: str, name: str | None = None,\n description: str | None = None, source_dialect: str | None = None,\n target_dialect: str | None = None, is_active: bool | None = None,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryCRUD.update_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n logger.reason(\"Updating dictionary\", {\"id\": dict_id})\n if name is not None:\n dictionary.name = name\n if description is not None:\n dictionary.description = description\n if source_dialect is not None:\n dictionary.source_dialect = source_dialect\n if target_dialect is not None:\n dictionary.target_dialect = target_dialect\n if is_active is not None:\n dictionary.is_active = is_active\n db.commit()\n db.refresh(dictionary)\n logger.reflect(\"Dictionary updated\", {\"id\": dictionary.id})\n return dictionary\n # endregion update_dictionary\n\n # region delete_dictionary [TYPE Function]\n # @PURPOSE: Delete a dictionary, blocked if attached to active/scheduled jobs.\n @staticmethod\n def delete_dictionary(db: Session, dict_id: str) -> None:\n with belief_scope(\"DictionaryCRUD.delete_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n\n blocked_statuses = [\"ACTIVE\", \"READY\", \"RUNNING\", \"SCHEDULED\"]\n attached_jobs = (\n db.query(TranslationJobDictionary)\n .filter(\n TranslationJobDictionary.dictionary_id == dict_id,\n TranslationJobDictionary.job_id.in_(\n db.query(TranslationJob.id).filter(TranslationJob.status.in_(blocked_statuses))\n ),\n )\n .count()\n )\n if attached_jobs > 0:\n logger.explore(\"Delete blocked: dictionary attached to active jobs\", {\"dict_id\": dict_id, \"jobs\": attached_jobs})\n raise ValueError(\n f\"Cannot delete dictionary attached to {attached_jobs} active/scheduled job(s). \"\n \"Remove dictionary associations from jobs first.\"\n )\n\n logger.reason(\"Deleting dictionary\", {\"id\": dict_id})\n db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.query(TranslationJobDictionary).filter(TranslationJobDictionary.dictionary_id == dict_id).delete()\n db.delete(dictionary)\n db.commit()\n logger.reflect(\"Dictionary deleted\", {\"id\": dict_id})\n # endregion delete_dictionary\n\n # region get_dictionary [TYPE Function]\n # @PURPOSE: Get a single dictionary by ID.\n @staticmethod\n def get_dictionary(db: Session, dict_id: str) -> TerminologyDictionary:\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n return dictionary\n # endregion get_dictionary\n\n # region list_dictionaries [TYPE Function]\n # @PURPOSE: List dictionaries with pagination.\n @staticmethod\n def list_dictionaries(\n db: Session, page: int = 1, page_size: int = 20,\n ) -> tuple[list[TerminologyDictionary], int]:\n total = db.query(func.count(TerminologyDictionary.id)).scalar() or 0\n dictionaries = (\n db.query(TerminologyDictionary)\n .order_by(TerminologyDictionary.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return dictionaries, total\n # endregion list_dictionaries\n\n# #endregion DictionaryCRUD\n" + }, + { + "contract_id": "DictionaryEntryCRUD", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/dictionary_entries.py", + "start_line": 1, + "end_line": 164, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "CRUD operations for DictionaryEntry records." + }, + "relations": [ + { + "source_id": "DictionaryEntryCRUD", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryEntry", + "target_ref": "[DictionaryEntry]" + }, + { + "source_id": "DictionaryEntryCRUD", + "relation_type": "DEPENDS_ON", + "target_id": "TerminologyDictionary", + "target_ref": "[TerminologyDictionary]" + } + ], + "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 ._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", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/dictionary_filter.py", + "start_line": 1, + "end_line": 131, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Scan batch texts for case-insensitive, word-boundary-aware matches against dictionaries." + }, + "relations": [ + { + "source_id": "DictionaryBatchFilter", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryEntry", + "target_ref": "[DictionaryEntry]" + }, + { + "source_id": "DictionaryBatchFilter", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJobDictionary", + "target_ref": "[TranslationJobDictionary]" + }, + { + "source_id": "DictionaryBatchFilter", + "relation_type": "DEPENDS_ON", + "target_id": "TerminologyDictionary", + "target_ref": "[TerminologyDictionary]" + }, + { + "source_id": "DictionaryBatchFilter", + "relation_type": "DEPENDS_ON", + "target_id": "ContextAwarePromptBuilder", + "target_ref": "[ContextAwarePromptBuilder]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region DictionaryBatchFilter [C:3] [TYPE Class] [SEMANTICS dictionary, filter, batch, matching]\n# @BRIEF Scan batch texts for case-insensitive, word-boundary-aware matches against dictionaries.\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n# @RELATION DEPENDS_ON -> [ContextAwarePromptBuilder]\n\nimport re\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n DictionaryEntry,\n TerminologyDictionary,\n TranslationJobDictionary,\n)\nfrom .prompt_builder import ContextAwarePromptBuilder\n\n\nclass DictionaryBatchFilter:\n \"\"\"Filter batch texts against dictionaries with word-boundary-aware matching.\"\"\"\n\n # region filter_for_batch [TYPE Function]\n # @PURPOSE: Scan batch texts for matches against all dictionaries attached to a job.\n @staticmethod\n def filter_for_batch(\n db: Session, source_texts: list[str], job_id: str,\n source_language: str | None = None,\n target_language: str | None = None,\n row_context: dict | None = None,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"DictionaryBatchFilter.filter_for_batch\"):\n job_dict_links = (\n db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n if not job_dict_links:\n logger.reason(\"No dictionaries attached to job\", {\"job_id\": job_id})\n return []\n\n dict_ids = [jd.dictionary_id for jd in job_dict_links]\n dictionaries = (\n db.query(TerminologyDictionary)\n .filter(TerminologyDictionary.id.in_(dict_ids), TerminologyDictionary.is_active == True)\n .all()\n )\n if not dictionaries:\n return []\n\n dict_map = {d.id: d.name for d in dictionaries}\n all_entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .order_by(DictionaryEntry.source_term.asc())\n .all()\n )\n if not all_entries:\n return []\n\n matched: list[dict[str, Any]] = []\n seen_source_match: set = set()\n\n for text_idx, source_text in enumerate(source_texts):\n if not source_text:\n continue\n text_lower = source_text.lower()\n\n for entry in all_entries:\n norm = entry.source_term_normalized\n if not norm:\n continue\n\n if source_language is not None:\n src = source_language.strip().lower()\n entry_src = entry.source_language.strip().lower()\n if entry_src != src and entry_src != \"und\":\n continue\n if target_language is not None:\n tgt = target_language.strip().lower()\n entry_tgt = entry.target_language.strip().lower()\n if entry_tgt != tgt:\n continue\n\n escaped = re.escape(norm)\n pattern = re.compile(r\"\\b\" + escaped + r\"\\b\", re.IGNORECASE)\n\n if pattern.search(text_lower):\n match_key = (text_idx, entry.id)\n if match_key not in seen_source_match:\n seen_source_match.add(match_key)\n\n priority_match = False\n if row_context and entry.has_context and entry.context_data:\n similarity = ContextAwarePromptBuilder.compute_context_similarity(\n entry.context_data, row_context\n )\n priority_match = similarity >= 0.5\n\n matched.append({\n \"text_index\": text_idx,\n \"source_text\": source_text,\n \"entry_id\": entry.id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"dictionary_id\": entry.dictionary_id,\n \"dictionary_name\": dict_map.get(entry.dictionary_id, \"\"),\n \"context_notes\": entry.context_notes,\n \"context_data\": entry.context_data,\n \"has_context\": entry.has_context,\n \"context_source\": entry.context_source,\n \"usage_notes\": entry.usage_notes,\n \"source_language\": entry.source_language,\n \"target_language\": entry.target_language,\n \"priority_match\": priority_match,\n })\n\n dict_priority = {d_id: idx for idx, d_id in enumerate(dict_ids)}\n matched.sort(key=lambda m: (dict_priority.get(m[\"dictionary_id\"], 999), m[\"text_index\"]))\n\n logger.reflect(\"Batch filter match complete\", {\n \"job_id\": job_id, \"source_texts\": len(source_texts),\n \"matches\": len(matched), \"dictionaries_used\": len(dictionaries),\n })\n return matched\n # endregion filter_for_batch\n\n# #endregion DictionaryBatchFilter\n" + }, + { + "contract_id": "DictionaryImportExport", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/dictionary_import_export.py", + "start_line": 1, + "end_line": 196, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Import/export entries as CSV/TSV with conflict detection, and migration of old entries." + }, + "relations": [ + { + "source_id": "DictionaryImportExport", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryEntry", + "target_ref": "[DictionaryEntry]" + }, + { + "source_id": "DictionaryImportExport", + "relation_type": "DEPENDS_ON", + "target_id": "TerminologyDictionary", + "target_ref": "[TerminologyDictionary]" + } + ], + "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 ._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", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/dictionary_validation.py", + "start_line": 1, + "end_line": 17, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Validate that a language tag is a non-empty BCP-47 string." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _validate_bcp47 [C:2] [TYPE Function] [SEMANTICS validation, bcp47, language]\n# @BRIEF Validate that a language tag is a non-empty BCP-47 string.\n\nimport re as _re\n\n\ndef _validate_bcp47(tag: str, field_name: str) -> None:\n \"\"\"Validate that tag is a non-empty BCP-47 string (basic check).\"\"\"\n if not tag or not tag.strip():\n raise ValueError(f\"{field_name} must be a non-empty BCP-47 language tag\")\n tag = tag.strip()\n if not _re.match(r'^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$', tag):\n raise ValueError(\n f\"{field_name} is not a valid BCP-47 tag: '{tag}'. \"\n \"Expected format like 'en', 'ru', 'zh-CN', 'pt-BR'.\"\n )\n# #endregion _validate_bcp47\n" }, { "contract_id": "TranslationEventLog", @@ -49099,181 +51683,55 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/executor.py", "start_line": 1, - "end_line": 1974, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input: TranslationRun + Job -> Output: updated Run with batch/record rows", - "INVARIANT": "Batch processing is independent — one batch failure does not affect others.", - "LAYER": "Domain", - "POST": "TranslationBatch and TranslationRecord rows are created. Run status is updated.", - "PRE": "Valid TranslationRun with job configuration. DB session is available.", - "PURPOSE": "Process translation in batches: fetch source rows, call LLM, persist TranslationBatch and TranslationRecord rows.", - "RATIONALE": "Batch processing with retry — independent batches allow partial recovery.", - "REJECTED": "Single monolithic LLM call — would lose all progress on any failure.", - "SIDE_EFFECT": "Calls LLM provider; creates DB rows; updates run statistics." - }, - "relations": [ - { - "source_id": "TranslationExecutor", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationBatch", - "target_ref": "[TranslationBatch]" - }, - { - "source_id": "TranslationExecutor", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationRecord", - "target_ref": "[TranslationRecord]" - }, - { - "source_id": "TranslationExecutor", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationRun", - "target_ref": "[TranslationRun]" - }, - { - "source_id": "TranslationExecutor", - "relation_type": "DEPENDS_ON", - "target_id": "LLMProviderService", - "target_ref": "[LLMProviderService]" - }, - { - "source_id": "TranslationExecutor", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" - }, - { - "source_id": "TranslationExecutor", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreview", - "target_ref": "[TranslationPreview]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region TranslationExecutor [C:5] [TYPE Module] [SEMANTICS sqlalchemy, tenacity, translate, insert, llm-retry]\n# @BRIEF Process translation in batches: fetch source rows, call LLM, persist TranslationBatch and TranslationRecord rows.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @PRE: Valid TranslationRun with job configuration. DB session is available.\n# @POST: TranslationBatch and TranslationRecord rows are created. Run status is updated.\n# @SIDE_EFFECT: Calls LLM provider; creates DB rows; updates run statistics.\n# @DATA_CONTRACT Input: TranslationRun + Job -> Output: updated Run with batch/record rows\n# @INVARIANT Batch processing is independent — one batch failure does not affect others.\n# @RATIONALE: Batch processing with retry — independent batches allow partial recovery.\n# @REJECTED: Single monolithic LLM call — would lose all progress on any failure.\n\nimport hashlib\nimport json\nimport re\nimport time\nimport uuid\nfrom collections.abc import Callable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationBatch,\n TranslationJob,\n TranslationLanguage,\n TranslationPreviewRecord,\n TranslationPreviewSession,\n TranslationRecord,\n TranslationRun,\n TranslationRunLanguageStats,\n)\nfrom ...services.llm_prompt_templates import render_prompt\nfrom ...services.llm_provider import LLMProviderService\nfrom ._token_budget import estimate_token_budget\nfrom .dictionary import DictionaryManager\nfrom .preview import DEFAULT_EXECUTION_PROMPT_TEMPLATE\nfrom .prompt_builder import ContextAwarePromptBuilder\nfrom .sql_generator import SQLGenerator, _normalize_timestamp_value\nfrom .superset_executor import SupersetSqlLabExecutor\n\n# #region MAX_RETRIES_PER_BATCH [TYPE Constant]\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\n\n# #region MAX_ROWS_PER_RUN [TYPE Constant]\n# Safety cap: without it, a datasource with thousands of rows blocks the single\n# uvicorn worker for minutes/hours. Preview uses sample_size (5-10). Full run\n# should stay within reasonable bounds.\nMAX_ROWS_PER_RUN = 10000\n# #endregion MAX_ROWS_PER_RUN\n\n# #region _enforce_dictionary [TYPE Function]\n# @BRIEF Post-processing: enforce dictionary entries in LLM output.\n# If a source term from the dictionary appears in the original text,\n# but the target term is missing from the translation, replace occurrences\n# of the source term in the translated output with the target term.\n# @PRE: dict_matches is a list of dict entries with source_term/target_term.\n# @POST: per_lang_values may be mutated to include forced dictionary replacements.\n# @SIDE_EFFECT: Logs when enforcement is applied.\ndef _enforce_dictionary(\n source_text: str,\n per_lang_values: dict[str, str],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n row_id: str,\n) -> None:\n \"\"\"Post-processing: enforce dictionary terms in LLM translation output.\n\n For each dictionary entry whose source_term appears in the source_text,\n verify that the target_term is present in each language's translation.\n If missing, replace any occurrence of the source term (which the LLM\n may have left untranslated) with the dictionary target term.\n \"\"\"\n if not dict_matches or not source_text:\n return\n\n text_lower = source_text.lower()\n\n for dm in dict_matches:\n src_term = dm.get(\"source_term\", \"\")\n tgt_term = dm.get(\"target_term\", \"\")\n if not src_term or not tgt_term:\n continue\n\n # Only process if source term actually appears in the original text\n if src_term.lower() not in text_lower:\n continue\n\n # Try to replace in each language\n for lang_code in list(per_lang_values.keys()):\n val = per_lang_values[lang_code]\n if not val:\n continue\n\n # Check if target term is already present (case-insensitive)\n if tgt_term.lower() in val.lower():\n continue\n\n # Try to find the source term (or a close variant) in the output\n # This catches cases where the LLM left the source term untranslated\n # Use lambda to prevent regex back-reference injection from tgt_term\n src_pattern = re.compile(re.escape(src_term), re.IGNORECASE)\n if src_pattern.search(val):\n new_val = src_pattern.sub(lambda _: tgt_term, val)\n if new_val != val:\n logger.reason(\"Dictionary enforcement applied\", {\n \"batch_id\": batch_id,\n \"row_id\": row_id,\n \"language_code\": lang_code,\n \"source_term\": src_term,\n \"target_term\": tgt_term,\n \"before\": val[:200],\n \"after\": new_val[:200],\n })\n per_lang_values[lang_code] = new_val\n# #endregion _enforce_dictionary\n\n\n# #region _compute_source_hash [TYPE Function]\n# @BRIEF Compute deterministic cache key for a source row.\n# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash).\n# Key columns (row_id, primary keys) are EXCLUDED — only the text content\n# and meaningful context affect the hash. This ensures identical text with\n# different row identifiers still produces a cache hit.\ndef _compute_source_hash(\n source_text: str,\n source_data: dict | None,\n dict_snapshot_hash: str | None,\n config_hash: str | None,\n context_keys: list[str] | None = None,\n) -> str:\n \"\"\"Deterministic cache key for a translation source row.\n\n Only includes source_text and context-relevant fields from source_data.\n Key/identifier columns are excluded so identical text in different rows hits cache.\n \"\"\"\n # Extract only context-relevant fields from source_data (not keys/identifiers)\n context_data: dict[str, str] = {}\n if source_data and context_keys:\n for key in context_keys:\n val = source_data.get(key)\n if val is not None:\n context_data[key] = str(val)\n\n payload = json.dumps({\n \"text\": source_text,\n \"ctx\": context_data, # only context columns, no key/row_id\n \"dict_hash\": dict_snapshot_hash or \"\",\n \"config_hash\": config_hash or \"\",\n }, sort_keys=True, ensure_ascii=False)\n return hashlib.sha256(payload.encode(\"utf-8\")).hexdigest()\n# #endregion _compute_source_hash\n\n\n# #region _check_translation_cache [TYPE Function]\n# @BRIEF Look up a previously successful translation by source_hash.\n# Returns per-language dict {lang_code: final_value} or None.\ndef _check_translation_cache(\n db: Session,\n source_hash: str,\n) -> dict[str, str] | None:\n \"\"\"Check if this source_hash was already translated successfully.\"\"\"\n from sqlalchemy.orm import joinedload\n from ...models.translate import TranslationRecord, TranslationLanguage\n\n cached = (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.source_hash == source_hash,\n TranslationRecord.status == \"SUCCESS\",\n )\n .order_by(TranslationRecord.created_at.desc())\n .first()\n )\n if not cached:\n return None\n\n lang_values: dict[str, str] = {}\n for lang in cached.languages:\n if lang.status == \"translated\" and lang.final_value:\n lang_values[lang.language_code] = lang.final_value\n\n return lang_values if lang_values else None\n# #endregion _check_translation_cache\n\n\n# #region estimate_row_tokens [C:4] [TYPE Function] [SEMANTICS translate, token, estimation]\n# @BRIEF Estimate token count for a single source row including context fields.\n# @PRE source_text is a string.\n# @POST Returns estimated token count >= 1.\n# @RELATION DEPENDS_ON -> [_estimate_tokens_for_text]\n\ndef estimate_row_tokens(\n source_text: str,\n source_data: dict | None,\n job,\n) -> int:\n \"\"\"Estimate token count for a single source row including context fields.\n\n Uses CJK-aware heuristics via _token_budget._estimate_tokens_for_text.\n Context fields from job.context_columns are included in the estimate.\n \"\"\"\n from ._token_budget import _estimate_tokens_for_text\n\n text_tokens = _estimate_tokens_for_text(source_text or \"\")\n\n context_keys = job.context_columns or []\n ctx_text = \"\"\n if source_data and context_keys:\n ctx_text = \" \".join(str(source_data.get(k, \"\")) for k in context_keys)\n ctx_tokens = _estimate_tokens_for_text(ctx_text)\n\n return text_tokens + ctx_tokens\n# #endregion estimate_row_tokens\n\n\n# #region TranslationExecutor [C:4] [TYPE Class]\n# @BRIEF Process translation batches: fetch source rows, filter dict, call LLM, persist results.\n# @PRE: DB session and config manager available.\n# @POST: Batches and records created with status tracking.\n# @SIDE_EFFECT: LLM API calls; DB writes.\nclass TranslationExecutor:\n\n def __init__(\n self,\n db: Session,\n 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._current_run_id: str | None = None\n self._preview_edits_cache: dict[str, dict[str, str]] | None = None # key_hash -> {lang_code: edited_value}\n\n # region _resolve_provider_model [TYPE Function]\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) -> 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 [TYPE Function]\n # @PURPOSE: Split source rows into variable-sized batches based on actual content length.\n # Uses estimate_token_budget to determine safe per-batch budgets and\n # _count_rows_that_fit logic to greedily fill each batch.\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 # @RATIONALE: Fixed batch_size of 50 wastes LLM context for short rows and\n # overflows for long rows. Variable sizing adapts to row content length,\n # maximizing 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 def _auto_size_batches(\n self,\n job,\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 Returns:\n List of batches, each batch is a list of row dicts.\n \"\"\"\n with belief_scope(\"TranslationExecutor._auto_size_batches\"):\n if not source_rows:\n return []\n\n # Resolve provider info if not provided\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 from estimate_token_budget\n # Pass all rows to get a global safe batch size estimate.\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 or returns zero, 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 # estimated_input_tokens includes PROMPT_BASE_TOKENS + dict_tokens +\n # sum of row tokens for the first `recommended` rows.\n # We subtract the prompt overhead to get the per-batch row budget.\n from ._token_budget import PROMPT_BASE_TOKENS\n\n estimated_input = budget.get(\"estimated_input_tokens\", 50000)\n per_batch_budget = estimated_input - PROMPT_BASE_TOKENS\n\n # Safety: if budget computation collapsed, fall back\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 \"estimated_input\": estimated_input,\n \"prompt_base\": PROMPT_BASE_TOKENS,\n \"fallback_size\": fallback_size,\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: accumulate rows until the next row would\n # exceed the per-batch token budget. Use output-aware hard cap too:\n # a single batch never exceeds 2x the recommended batch size as\n # a safety net against wildly oversized estimates.\n max_rows_hard_cap = max(recommended * 2, 20)\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 # ── Edge case: single row exceeds entire per-batch budget ──\n if rt > per_batch_budget:\n # Flush current batch first\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 # ── Start a new batch if: ──\n # a) Batch is non-empty AND adding this row would exceed budget\n # b) Hard row-count cap reached\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\n # region execute_run [TYPE Function]\n # @PURPOSE: Run full translation execution for a TranslationRun.\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,\n 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 with belief_scope(\"TranslationExecutor.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found for run '{run.id}'\")\n\n logger.reason(\"Starting translation execution\", {\n \"run_id\": run.id,\n \"job_id\": job.id,\n \"configured_batch_size\": job.batch_size,\n })\n\n # Load preview edits for carry-forward\n self._load_preview_edits(job.id)\n\n # Mark run as RUNNING\n run.status = \"RUNNING\"\n run.started_at = datetime.now(UTC)\n self.db.flush()\n\n # Determine whether this is a full translation (all rows) or incremental (new/changed only)\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 # Fetch source rows from the datasource\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\n\n # Apply new-key-only filtering for scheduled runs OR manual incremental runs\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\", {\n \"job_id\": job.id, \"run_id\": run.id,\n })\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\n\n total_rows = len(source_rows)\n run.total_records = total_rows\n\n # Resolve target languages for batch sizing\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\n # Split into auto-sized batches based on content length\n provider_model = self._resolve_provider_model(job)\n batches = self._auto_size_batches(\n job=job,\n source_rows=source_rows,\n target_languages=target_languages,\n provider_info=provider_model,\n )\n\n logger.reason(f\"Processing {len(batches)} batches\", {\n \"run_id\": run.id,\n \"total_rows\": total_rows,\n \"num_batches\": len(batches),\n \"avg_batch_size\": round(sum(len(b) for b in batches) / max(1, len(batches)), 1),\n \"max_batch_size\": max(len(b) for b in batches) if batches else 0,\n })\n\n successful_records = 0\n failed_records = 0\n skipped_records = 0\n\n for batch_idx, batch_rows in enumerate(batches):\n batch_result = self._process_batch(\n job=job,\n run_id=run.id,\n batch_index=batch_idx,\n batch_rows=batch_rows,\n dict_snapshot_hash=run.dict_snapshot_hash,\n config_hash=run.config_hash,\n )\n successful_records += batch_result[\"successful\"]\n failed_records += batch_result[\"failed\"]\n skipped_records += batch_result[\"skipped\"]\n\n # Update run stats incrementally\n run.successful_records = successful_records\n run.failed_records = failed_records\n run.skipped_records = skipped_records\n\n # Commit after each batch to release row locks and make RUNNING\n # status + batch progress visible to other DB sessions (frontend polling).\n self.db.commit()\n\n # Incremental INSERT into target table after each batch,\n # so data appears incrementally in the target view/table\n # without waiting for the entire run to complete.\n batch_id = batch_result.get(\"batch_id\")\n if batch_id and batch_result[\"successful\"] > 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, continuing)\", {\n \"batch_id\": batch_id,\n \"error\": str(e),\n })\n\n # Re-fetch run after commit to check for cancellation flag set by\n # cancel_run() fallback (direct SQL UPDATE of error_message).\n self.db.refresh(run)\n if run.error_message == \"CANCEL_REQUESTED\":\n logger.reason(\"Cancellation requested — stopping batch processing\", {\n \"run_id\": run.id,\n \"batch_index\": batch_idx,\n })\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(UTC)\n run.error_message = None # Clear the orchestration flag\n self.db.commit()\n return run\n\n # Update per-language statistics incrementally after each batch\n # so the frontend shows real-time per-language counts for RUNNING runs.\n if language_stats_map and batch_result[\"successful\"] > 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)\", {\n \"batch_id\": batch_id,\n \"error\": str(e),\n })\n\n if self.on_batch_progress:\n self.on_batch_progress(\n run.id, batch_idx + 1, len(batches),\n successful_records, total_rows,\n )\n\n # Update final run status\n if failed_records == 0 and skipped_records == 0:\n run.status = \"COMPLETED\"\n elif successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\" # Partial success\n\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n\n logger.reflect(\"Translation execution complete\", {\n \"run_id\": run.id,\n \"status\": run.status,\n \"total\": total_rows,\n \"successful\": successful_records,\n \"failed\": failed_records,\n \"skipped\": skipped_records,\n })\n\n return run\n # endregion execute_run\n\n # region _fetch_source_rows [TYPE Function]\n # @PURPOSE: Fetch full source dataset from Superset (via datasource) for full translation.\n # @PRE: job_id exists. Job may have source_datasource_id for full fetch.\n # @POST: Returns list of dicts with source data (all rows from the source datasource).\n # @SIDE_EFFECT: Makes HTTP call to Superset chart data API when datasource is configured.\n def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_source_rows\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n\n # If source_datasource_id is configured, fetch ALL rows from the Superset chart data API\n if job and job.source_datasource_id:\n try:\n logger.reason(\"Fetching full dataset from Superset datasource\", {\n \"run_id\": run_id,\n \"datasource_id\": job.source_datasource_id,\n \"environment_id\": job.environment_id,\n })\n\n # Determine environment\n environments = self.config_manager.get_environments()\n target_env_id = job.environment_id or job.source_dialect or \"\"\n env_config = next(\n (e for e in environments if e.id == target_env_id),\n None,\n )\n if not env_config and environments:\n env_config = environments[0]\n\n if env_config:\n from ...core.superset_client import SupersetClient\n client = SupersetClient(env_config)\n\n # Fetch dataset detail to build proper query context\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n\n # Build query context (same approach as preview but without row_limit)\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id),\n dataset_record=dataset_detail,\n template_params={},\n effective_filters=[],\n )\n\n # Use a safety cap instead of removing row_limit entirely.\n # Preview uses sample_size (5-10). Full runs should not fetch unlimited rows\n # because each batch of 20 rows × 4 languages takes ~40s of LLM time.\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = MAX_ROWS_PER_RUN\n queries[0].pop(\"result_type\", None)\n queries[0][\"metrics\"] = []\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n\n # Extract rows\n rows = self._extract_chart_data_rows(response)\n\n if rows:\n logger.reason(f\"Fetched {len(rows)} rows from Superset datasource\", {\n \"run_id\": run_id,\n })\n # Map rows to source_rows format\n source_rows = []\n for idx, row in enumerate(rows):\n source_data_dict = dict(row) if row else None\n source_text = str(row.get(job.translation_column, \"\")) if job.translation_column else json.dumps(row)\n source_rows.append({\n \"row_index\": str(idx),\n \"source_text\": source_text,\n \"approved_translation\": None,\n \"source_object_name\": f\"Row {idx}\",\n \"source_data\": source_data_dict,\n })\n return source_rows\n else:\n logger.explore(\"Superset datasource returned no rows\", {\n \"run_id\": run_id,\n \"datasource_id\": job.source_datasource_id,\n })\n else:\n logger.explore(\"No environment config found for datasource fetch\", {\n \"env_id\": target_env_id,\n })\n except Exception as e:\n logger.explore(\"Failed to fetch full dataset from Superset, falling back to preview\", {\n \"run_id\": run_id,\n \"error\": str(e),\n })\n # Fall through to preview-based fetch\n\n # Fallback: get the latest APPLIED preview session\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n logger.explore(\"No accepted preview session found\", {\"job_id\": job_id})\n return []\n\n # Fetch APPROVED or all records from the session\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.session_id == session.id,\n TranslationPreviewRecord.status.in_([\"APPROVED\", \"PENDING\"]),\n )\n .all()\n )\n\n source_rows = []\n for rec in records:\n source_data_dict = None\n if hasattr(rec, \"source_data\") and rec.source_data:\n source_data_dict = dict(rec.source_data)\n source_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": rec.target_sql if rec.status == \"APPROVED\" else None,\n \"source_object_name\": rec.source_object_name or \"\",\n \"source_data\": source_data_dict,\n })\n\n logger.reason(f\"Fetched {len(source_rows)} source rows from preview fallback\", {\n \"run_id\": run_id,\n \"session_id\": session.id,\n })\n return source_rows\n # endregion _fetch_source_rows\n\n # region _filter_new_keys [TYPE Function]\n # @PURPOSE: Filter source rows to only include those with keys absent from the last successful run.\n # @PRE: job and run are persisted; source_rows is a list of dicts with source_data containing key columns.\n # @POST: Returns filtered list of source rows with only new keys. Logs skip count.\n # @SIDE_EFFECT: Queries TranslationRecord from previous runs; no writes.\n def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:\n with belief_scope(\"TranslationExecutor._filter_new_keys\"):\n # Find most recent COMPLETED run with successful insert for this job\n prev_run = (\n self.db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job.id,\n TranslationRun.status == \"COMPLETED\",\n TranslationRun.insert_status == \"succeeded\",\n TranslationRun.id != run_id,\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if not prev_run:\n logger.reason(\"No prior successful run — all keys treated as new\", {\n \"job_id\": job.id,\n })\n return source_rows\n\n # Get successful records from that run\n prev_records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == prev_run.id,\n TranslationRecord.status == \"SUCCESS\",\n )\n .all()\n )\n if not prev_records:\n return source_rows\n\n # Build set of already-translated composite keys\n key_cols = job.target_key_cols or job.source_key_cols or []\n if not key_cols:\n logger.explore(\"No key columns configured — skipping new-key-only filter\",\n {\"job_id\": job.id})\n return source_rows\n existing_keys = set()\n for rec in prev_records:\n sd = rec.source_data or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n existing_keys.add(key_tuple)\n\n # Filter: keep only rows whose keys are NOT in the existing set\n filtered = []\n skipped = 0\n for row in source_rows:\n sd = row.get(\"source_data\", {}) or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n if key_tuple not in existing_keys:\n filtered.append(row)\n else:\n skipped += 1\n\n logger.reason(f\"New-key-only filter: {len(source_rows)} total → {len(filtered)} new, {skipped} skipped\", {\n \"job_id\": job.id,\n \"prev_run_id\": prev_run.id,\n \"key_cols\": key_cols,\n })\n return filtered\n # endregion _filter_new_keys\n\n # region _extract_chart_data_rows [TYPE Function]\n # @PURPOSE: Extract data rows from Superset chart data API response.\n # @POST: Returns list of dicts with column-value pairs.\n @staticmethod\n def _extract_chart_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, list):\n return result\n return []\n # endregion _extract_chart_data_rows\n\n # region _load_preview_edits [TYPE Function]\n # @PURPOSE: Load user edits from accepted preview session for carry-forward.\n # @PRE: job_id exists.\n # @POST: Populates _preview_edits_cache with key_hash -> {lang_code: edited_value}.\n # @SIDE_EFFECT: Queries TranslationPreviewLanguage and TranslationPreviewRecord.\n def _load_preview_edits(self, job_id: str) -> None:\n \"\"\"Load preview edits for carry-forward during execution.\"\"\"\n from ...models.translate import TranslationPreviewLanguage, TranslationPreviewRecord, TranslationPreviewSession\n\n with belief_scope(\"TranslationExecutor._load_preview_edits\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n logger.reason(\"No applied preview session found — no edits to carry forward\", {\n \"job_id\": job_id,\n })\n self._preview_edits_cache = {}\n return\n\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id)\n .all()\n )\n\n edits: dict[str, dict[str, str]] = {}\n for rec in records:\n if not rec.source_data:\n continue\n # Compute key hash from source_data\n key_hash = self._compute_key_hash(rec.source_data)\n edited_langs: dict[str, str] = {}\n for lang_entry in (rec.languages or []):\n if lang_entry.status in (\"edited\", \"approved\") and lang_entry.user_edit:\n edited_langs[lang_entry.language_code] = lang_entry.final_value or lang_entry.user_edit\n logger.reason(\"Carrying forward preview edit\", {\n \"key_hash\": key_hash,\n \"language_code\": lang_entry.language_code,\n })\n if edited_langs:\n edits[key_hash] = edited_langs\n\n self._preview_edits_cache = edits\n logger.reason(f\"Loaded {len(edits)} preview edits for carry-forward\", {\n \"job_id\": job_id,\n })\n # endregion _load_preview_edits\n\n # region _compute_key_hash [TYPE Function]\n # @PURPOSE: Compute a stable hash from source_data dict for matching preview edits.\n @staticmethod\n def _compute_key_hash(source_data: dict) -> str:\n import hashlib\n stable = json.dumps(source_data, sort_keys=True)\n return hashlib.sha256(stable.encode()).hexdigest()[:16]\n # endregion _compute_key_hash\n\n # region _process_batch [TYPE Function]\n # @PURPOSE: Process a single batch: filter dict, build prompt, call LLM, persist records.\n # @PRE: job and batch_rows are valid.\n # @POST: TranslationBatch and TranslationRecord rows are created.\n # @SIDE_EFFECT: LLM API call.\n def _process_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_index: int,\n batch_rows: list[dict[str, Any]],\n dict_snapshot_hash: str | None = None,\n config_hash: str | None = None,\n ) -> dict[str, int]:\n with belief_scope(\"TranslationExecutor._process_batch\"):\n batch_start = time.monotonic()\n\n # Create batch record\n batch = TranslationBatch(\n id=str(uuid.uuid4()),\n run_id=run_id,\n batch_index=batch_index,\n status=\"RUNNING\",\n total_records=len(batch_rows),\n started_at=datetime.now(UTC),\n )\n self.db.add(batch)\n self.db.flush()\n batch_id = batch.id\n\n result = {\"successful\": 0, \"failed\": 0, \"skipped\": 0, \"retries\": 0}\n\n # Extract source texts for dict filtering\n source_texts = [\n row.get(\"source_text\", \"\")\n for row in batch_rows\n if row.get(\"source_text\")\n ]\n\n # Filter dictionary entries with row context for priority matching\n row_context = batch_rows[0].get(\"source_data\") if batch_rows else None\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, source_texts, job.id,\n row_context=row_context,\n )\n\n # ── Translation cache check: skip LLM for rows translated before ──\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n continue\n source_text = row.get(\"source_text\", \"\")\n if not source_text:\n continue\n source_data = row.get(\"source_data\")\n # Build context_keys from job config (exclude key columns from hash)\n ctx_keys = list(job.context_columns or [])\n source_hash = _compute_source_hash(\n source_text, source_data,\n dict_snapshot_hash, config_hash,\n context_keys=ctx_keys,\n )\n row[\"_source_hash\"] = source_hash\n cached = _check_translation_cache(self.db, source_hash)\n if cached:\n row[\"_cached_lang_values\"] = cached\n logger.reason(\"Translation cache hit\", {\n \"source_hash\": source_hash[:12],\n \"langs\": list(cached.keys()),\n })\n\n # For each row, determine if we need LLM translation or can use approved/preview edit/cache\n rows_for_llm = []\n pre_translated = []\n\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n pre_translated.append(row)\n continue\n\n # Translation cache hit → treat as pre-translated\n # BUT only if cached langs cover ALL target languages\n cached_langs = row.get(\"_cached_lang_values\")\n if cached_langs:\n target_langs_for_check = job.target_languages or [job.target_dialect or \"en\"]\n if not isinstance(target_langs_for_check, list):\n target_langs_for_check = [str(target_langs_for_check)]\n cached_covers_all = all(\n lang_code in cached_langs\n for lang_code in target_langs_for_check\n )\n if cached_covers_all:\n pre_translated.append(row)\n continue\n else:\n logger.reason(\"Partial cache hit — missing languages, routing to LLM\", {\n \"cached_langs\": list(cached_langs.keys()),\n \"target_langs\": target_langs_for_check,\n })\n\n # Check for preview edits carry-forward\n source_data = row.get(\"source_data\") or {}\n if source_data and self._preview_edits_cache:\n key_hash = self._compute_key_hash(source_data)\n preview_edit = self._preview_edits_cache.get(key_hash)\n if preview_edit:\n # Use the first edited language's value as the approved translation\n first_edit = next(iter(preview_edit.values()), None)\n if first_edit:\n row[\"approved_translation\"] = first_edit\n logger.reason(\"Using preview edit carry-forward\", {\n \"key_hash\": key_hash,\n \"langs\": list(preview_edit.keys()),\n })\n pre_translated.append(row)\n continue\n\n rows_for_llm.append(row)\n\n # Handle pre-translated (approved) rows\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\n for row in pre_translated:\n cached_langs = row.get(\"_cached_lang_values\") # None for approved, dict for cache hits\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=row.get(\"approved_translation\", \"\"),\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n\n # Create per-language entry for each target language\n for lang_code in target_languages:\n if cached_langs and lang_code in cached_langs:\n final_val = cached_langs[lang_code]\n else:\n final_val = row.get(\"approved_translation\", \"\")\n lang_entry = TranslationLanguage(\n id=str(uuid.uuid4()),\n record_id=record.id,\n language_code=lang_code,\n source_language_detected=\"und\",\n translated_value=final_val,\n final_value=final_val,\n status=\"translated\",\n needs_review=False,\n )\n self.db.add(lang_entry)\n result[\"successful\"] += 1\n\n # Process rows needing LLM translation\n if rows_for_llm:\n # Resolve provider model for provider-aware token budget\n provider_model = None\n if job.provider_id:\n try:\n p_svc = LLMProviderService(self.db)\n p = p_svc.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 # Check token budget for this batch to determine safe max_tokens\n token_budget = estimate_token_budget(\n source_rows=rows_for_llm,\n target_languages=target_languages,\n source_column=\"source_text\",\n context_columns=None, # Context is embedded in dict, not separate column\n dictionary_entries=dict_matches,\n batch_size=len(rows_for_llm),\n provider_info=provider_model,\n )\n if token_budget[\"warning\"]:\n logger.explore(\"Token budget warning for batch\", {\n \"batch_id\": batch_id,\n \"batch_index\": batch_index,\n \"warning\": token_budget[\"warning\"],\n })\n\n llm_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=rows_for_llm,\n dict_matches=dict_matches,\n batch_id=batch_id,\n max_tokens=token_budget[\"max_output_needed\"],\n )\n result[\"successful\"] += llm_result[\"successful\"]\n result[\"failed\"] += llm_result[\"failed\"]\n result[\"skipped\"] += llm_result[\"skipped\"]\n result[\"retries\"] += llm_result.get(\"retries\", 0)\n\n # Update batch status\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(UTC)\n batch.status = \"COMPLETED\" if result[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n batch_latency = int((time.monotonic() - batch_start) * 1000)\n logger.reason(f\"Batch {batch_index} complete\", {\n \"batch_id\": batch_id,\n \"latency_ms\": batch_latency,\n **result,\n })\n\n return {**result, \"batch_id\": batch_id}\n # endregion _process_batch\n\n # region _insert_batch_to_target [TYPE Function]\n # @PURPOSE: Insert successful records from a single batch into the target table via Superset SQL Lab.\n # @PRE: batch has committed TranslationRecords with status SUCCESS. job has target_table configured.\n # @POST: Per record, N+1 rows are INSERTED: 1 original + N translations.\n # Context columns are bundled into JSON in the `context` column.\n # `is_original=1` marks the source-language row.\n # @SIDE_EFFECT: HTTP call to Superset SQL Lab API. Writes to target database.\n def _insert_batch_to_target(\n self,\n job: TranslationJob,\n batch_id: str,\n run_id: str,\n ) -> None:\n with belief_scope(\"TranslationExecutor._insert_batch_to_target\"):\n records = (\n self.db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.batch_id == batch_id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n return\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n\n # Columns that exist in the target ClickHouse table\n columns = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n # Deduplicate while preserving order\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n columns = deduped\n\n # Keys for the context JSON: context_columns + original translation_column\n context_keys = list(job.context_columns or [])\n if job.translation_column and job.translation_column != effective_target and job.translation_column not in context_keys:\n context_keys.append(job.translation_column)\n\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n\n # Detect source language from first TranslationLanguage entry\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n # Build context JSON: all extra columns the user configured\n context_data: dict[str, str] = {}\n for key in context_keys:\n val = source_data.get(key)\n context_data[key] = str(val) if val is not None else \"\"\n\n # ── Shared base row (columns common to original and all translations) ──\n base_row: dict[str, object] = {}\n\n # Key columns (report_date, document_number) — normalize timestamps to YYYY-MM-DD\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n\n # Source text: original\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n\n # Source language (same for all rows of this record)\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n\n # Context JSON string\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n # ── 1. ORIGINAL row (is_original = 1) ──\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n # ── 2. TRANSLATION rows (is_original = 0) ──\n # Skip language that matches the source — the original row already covers it\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n # Fallback: no per-language data → single translation row with primary language\n fallback_row = dict(base_row)\n if effective_target:\n fallback_row[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback_row[job.target_language_column] = primary_language\n fallback_row[\"is_original\"] = 0\n rows_for_sql.append(fallback_row)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(self.config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend for batch insert\", {\n \"batch_id\": batch_id,\n \"error\": str(e),\n })\n real_backend = None\n\n dialect = real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n except ValueError as e:\n logger.explore(\"SQL generation failed for batch\", {\n \"batch_id\": batch_id,\n \"error\": str(e),\n })\n return\n\n try:\n result = executor.execute_and_poll(\n sql=sql,\n max_polls=30,\n poll_interval_seconds=2.0,\n )\n except Exception as e:\n logger.explore(\"Superset SQL submission failed for batch\", {\n \"batch_id\": batch_id,\n \"error\": str(e),\n })\n return\n\n logger.reason(f\"Batch {batch_id[:12]} inserted {row_count} rows\", {\n \"batch_id\": batch_id,\n \"rows\": row_count,\n \"status\": result.get(\"status\"),\n })\n # endregion _insert_batch_to_target\n\n # region _update_language_stats_incremental [TYPE Function]\n # @PURPOSE: Update per-language TranslationRunLanguageStats incrementally after each batch.\n # @PRE: language_stats_map has entries for all target languages.\n # @POST: Language stat objects updated with counts from committed TranslationLanguage rows.\n # @SIDE_EFFECT: Mutates ORM objects; caller must commit.\n def _update_language_stats_incremental(\n self,\n run_id: str,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n ) -> None:\n with belief_scope(\"TranslationExecutor._update_language_stats_incremental\"):\n records = (\n self.db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == run_id)\n .all()\n )\n record_ids = [r.id for r in records]\n if not record_ids:\n return\n\n lang_entries = (\n self.db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids))\n .all()\n )\n\n from collections import defaultdict\n agg: dict[str, dict[str, int]] = defaultdict(\n lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0}\n )\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_tokens_est = max(1, sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value) // 4)\n num_langs = len(language_stats_map) or 1\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n lang_stat.token_count = total_tokens_est // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n\n self.db.flush()\n # endregion _update_language_stats_incremental\n\n # region _call_llm_for_batch [TYPE Function]\n # @PURPOSE: Call LLM for a batch of rows requiring translation. Parse structured JSON response.\n # @PRE: job has valid provider_id. batch_rows is non-empty.\n # @POST: Returns dict with successful/failed/skipped counts. Creates TranslationRecord rows.\n # @SIDE_EFFECT: HTTP call to LLM provider.\n def _call_llm_for_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_rows: list[dict[str, Any]],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n max_tokens: int = 8192,\n _recursion_depth: int = 0,\n ) -> dict[str, int]:\n with belief_scope(\"TranslationExecutor._call_llm_for_batch\"):\n # Build dictionary section using ContextAwarePromptBuilder\n dictionary_section = \"\"\n if dict_matches:\n # Get row_context from first batch row if available\n row_context = batch_rows[0].get(\"source_data\") if batch_rows else None\n\n # Use ContextAwarePromptBuilder for context-aware annotations\n annotated_entries = ContextAwarePromptBuilder.build_context_entries(\n dict_matches, row_context\n )\n glossary_lines = []\n for annotated_line in annotated_entries:\n glossary_lines.append(f\"- {annotated_line}\")\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n # Resolve target languages\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 target_languages_str = \", \".join(target_languages)\n\n # Build rows JSON for LLM\n rows_json = json.dumps([\n {\n \"row_id\": str(row.get(\"row_index\", idx)),\n \"text\": row.get(\"source_text\", \"\"),\n }\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n\n # Build prompt (use multi-language format)\n prompt = render_prompt(\n DEFAULT_EXECUTION_PROMPT_TEMPLATE,\n {\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\n # Call LLM with retry\n llm_response = None\n last_error = None\n retries = 0\n\n finish_reason: str | None = 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})\", {\n \"batch_id\": batch_id,\n \"error\": last_error,\n \"attempt\": attempt,\n })\n if attempt < MAX_RETRIES_PER_BATCH:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n logger.explore(\"LLM call exhausted retries\", {\n \"batch_id\": batch_id,\n \"last_error\": last_error,\n })\n\n if llm_response is None:\n # All retries failed — mark all rows as failed\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"FAILED\",\n error_message=f\"LLM call failed after {retries} retries: {last_error}\",\n )\n self.db.add(record)\n return {\"successful\": 0, \"failed\": len(batch_rows), \"skipped\": 0, \"retries\": retries}\n\n # ── Truncation detection: finish_reason=\"length\" → split batch ──\n if finish_reason == \"length\" and len(batch_rows) >= 2 and run_id:\n if _recursion_depth < MAX_RETRIES_PER_BATCH:\n mid = len(batch_rows) // 2\n logger.explore(\"LLM output truncated — splitting batch\", {\n \"batch_id\": batch_id,\n \"batch_size\": len(batch_rows),\n \"split_at\": mid,\n \"recursion_depth\": _recursion_depth,\n \"finish_reason\": finish_reason,\n })\n left_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=batch_rows[:mid],\n dict_matches=dict_matches,\n batch_id=batch_id + \"_L\",\n max_tokens=max_tokens,\n _recursion_depth=_recursion_depth + 1,\n )\n right_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=batch_rows[mid:],\n dict_matches=dict_matches,\n batch_id=batch_id + \"_R\",\n max_tokens=max_tokens,\n _recursion_depth=_recursion_depth + 1,\n )\n merged = {\n \"successful\": left_result[\"successful\"] + right_result[\"successful\"],\n \"failed\": left_result[\"failed\"] + right_result[\"failed\"],\n \"skipped\": left_result[\"skipped\"] + right_result[\"skipped\"],\n \"retries\": retries + left_result.get(\"retries\", 0) + right_result.get(\"retries\", 0),\n }\n logger.reason(\"Truncation resolved by batch splitting\", {\n \"batch_id\": batch_id,\n \"left_successful\": left_result[\"successful\"],\n \"right_successful\": right_result[\"successful\"],\n \"left_size\": len(batch_rows[:mid]),\n \"right_size\": len(batch_rows[mid:]),\n })\n return merged\n else:\n logger.explore(\"Truncation recursion depth exceeded — accepting truncated output\", {\n \"batch_id\": batch_id,\n \"recursion_depth\": _recursion_depth,\n \"max_depth\": MAX_RETRIES_PER_BATCH,\n })\n # Fall through to parse truncated response\n\n # Parse LLM response (multi-language aware)\n try:\n translations = self._parse_llm_response(llm_response, len(batch_rows), target_languages=target_languages)\n except ValueError as e:\n # Parse failure — mark all rows as SKIPPED\n logger.explore(\"LLM response parse failed\", {\n \"batch_id\": batch_id,\n \"error\": str(e),\n \"response_preview\": llm_response[:500] if llm_response else \"\",\n })\n skipped = len(batch_rows)\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SKIPPED\",\n error_message=f\"LLM parse failure: {e}\",\n )\n self.db.add(record)\n return {\n \"successful\": 0,\n \"failed\": 0,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n\n successful = 0\n failed = 0\n skipped = 0\n\n for row in batch_rows:\n row_id = str(row.get(\"row_index\", \"\"))\n translation_data = translations.get(row_id)\n source_text = row.get(\"source_text\", \"\")\n detected_lang = \"und\"\n if translation_data:\n detected_lang = translation_data.get(\"detected_source_language\", \"und\")\n\n if translation_data is None:\n # NULL translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=source_text,\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SKIPPED\",\n error_message=\"NULL translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n # Collect per-language translated values\n per_lang_values: dict[str, str] = {}\n has_any_translation = False\n\n # First try multi-language format (per-language keys)\n for lang_code in target_languages:\n lang_val = translation_data.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n per_lang_values[lang_code] = str(lang_val)\n has_any_translation = True\n\n # Fallback to legacy single \"translation\" key format\n if not has_any_translation:\n translation_text = translation_data.get(\"translation\", \"\")\n if translation_text.strip():\n per_lang_values[target_languages[0]] = translation_text\n has_any_translation = True\n\n # ── Dictionary post-processing enforcement ──\n # If a dictionary entry matches the source text but the target term\n # is missing from the LLM output, apply forced replacement.\n if dict_matches and source_text:\n _enforce_dictionary(source_text, per_lang_values, dict_matches,\n batch_id, row_id)\n # ── End dictionary enforcement ──\n\n if not has_any_translation:\n # Empty/all-empty translations — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=source_text,\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SKIPPED\",\n error_message=\"Empty translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n # Use the first language's value as the primary target_sql for backward compat\n primary_translation = next(iter(per_lang_values.values()), \"\")\n\n successful += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=source_text,\n target_sql=primary_translation,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n\n # Create per-language entries — one TranslationLanguage per language_code\n for lang_code in target_languages:\n # Skip if this language matches the detected source language —\n # no translation needed, and it shouldn't appear in stats\n if detected_lang != \"und\" and str(lang_code).lower() == str(detected_lang).lower():\n continue\n\n lang_translation = per_lang_values.get(lang_code, \"\")\n\n # Check for undetermined source language\n lang_needs_review = (detected_lang == \"und\")\n\n if lang_needs_review:\n logger.explore(\"undetected language\", {\n \"record_id\": row_id,\n \"language_code\": lang_code,\n \"source_text\": source_text[:100],\n })\n\n lang_entry = TranslationLanguage(\n id=str(uuid.uuid4()),\n record_id=record.id,\n language_code=lang_code,\n source_language_detected=detected_lang,\n translated_value=lang_translation or \"\",\n final_value=lang_translation or \"\",\n status=\"translated\",\n needs_review=lang_needs_review,\n )\n self.db.add(lang_entry)\n\n return {\n \"successful\": successful,\n \"failed\": failed,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n # endregion _call_llm_for_batch\n\n # region _call_llm [TYPE Function]\n # @PURPOSE: Call the configured LLM provider with the batch prompt.\n # @PRE: job has valid provider_id.\n # @POST: Returns raw LLM response string.\n # @SIDE_EFFECT: HTTP call to LLM provider.\n def _call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> tuple[str, str | None]:\n with belief_scope(\"TranslationExecutor._call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n\n disable_reasoning = getattr(job, 'disable_reasoning', False)\n\n if provider_type in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\", \"litellm\"):\n response_text, finish_reason = self._call_openai_compatible(\n base_url=provider.base_url,\n api_key=api_key,\n model=model,\n prompt=prompt,\n provider_type=provider_type,\n max_tokens=max_tokens,\n disable_reasoning=disable_reasoning,\n )\n return response_text, finish_reason\n else:\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n # endregion _call_llm\n\n # region _call_openai_compatible [TYPE Function]\n # @PURPOSE: Call OpenAI-compatible API for batch translation.\n # @PRE: Valid API endpoint, key, model, and prompt.\n # @POST: Returns response text.\n # @SIDE_EFFECT: HTTP POST to LLM API.\n @staticmethod\n def _call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n ) -> tuple[str, str | None]:\n with belief_scope(\"TranslationExecutor._call_openai_compatible\"):\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n # Reasoning-safe system prompt: always forbid chain-of-thought in output.\n # Reasoning models (DeepSeek, QwQ, etc.) may leak reasoning into content\n # when response_format=json_object is set. Explicit suppression in the\n # system message prevents this regardless of the disable_reasoning flag.\n system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n # Structured output — OpenRouter and Kilo also support response_format, but some\n # upstream providers (e.g. StepFun) reject it. We try with response_format and\n # fall back on 400 \"structured_outputs is not supported\".\n # NOTE: Reasoning models (deepseek, qwen with reasoning, etc.) often break with\n # response_format=json_object, producing reasoning text instead of JSON.\n # When disable_reasoning is set, we remove response_format and rely on the\n # system prompt to enforce JSON-only output.\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n # Suppress Chain of Thought reasoning to save output tokens\n # NOTE: Kilo/OpenRouter/LiteLLM do NOT support disabling reasoning (returns 400)\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n # response_format already skipped above when disable_reasoning is True\n # Use caller-provided max_tokens instead of hardcoded 8192\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n\n # ── Handle rate limiting with Retry-After header ──\n import time as _time\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(\n f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} \"\n f\"after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait},\n )\n _time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n\n _response_format_error_patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response.text or \"\").lower() for p in _response_format_error_patterns)\n ):\n logger.explore(\"Structured outputs not supported by upstream, retrying without response_format\", extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"body={response.text[:2000]}\"\n )\n response.raise_for_status()\n data = response.json()\n\n choices = data.get(\"choices\", [])\n if not choices:\n logger.explore(\"LLM returned no choices\", extra={\"src\": \"executor\", \"response_keys\": list(data.keys()), \"response_preview\": str(data)[:2000]})\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\",\n \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__,\n \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n # Handle model refusal (content is empty/null, refusal field has reason)\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\",\n \"refusal\": str(refusal)[:500],\n \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n logger.reason(f\"LLM response finish_reason={finish_reason} content_len={len(content)} msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\")\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\"src\": \"executor\", \"finish_reason\": finish_reason, \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [], \"response_preview\": str(data)[:2000]})\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n # endregion _call_openai_compatible\n\n # region _parse_llm_response [TYPE Function]\n # @PURPOSE: Parse LLM JSON response into dict of row_id -> per-language translations.\n # Supports multi-language format: {\"row_id\": 0, \"detected_source_language\": \"fr\", \"ru\": \"текст\", \"en\": \"text\"}\n # Backward-compat: old format {\"row_id\": 0, \"translation\": \"text\", \"detected_source_language\": \"fr\"}\n # @PRE: response_text is valid JSON with {\"rows\": [...]} structure.\n # @POST: Returns dict mapping row_id to dict with 'detected_source_language' and per-language codes.\n @staticmethod\n def _parse_llm_response(response_text: str, expected_count: int, target_languages: list[str] | None = None, finish_reason: str | None = None) -> dict[str, dict[str, str]]:\n with belief_scope(\"TranslationExecutor._parse_llm_response\"):\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n # Try to extract from markdown code block\n import re\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n rows = data.get(\"rows\", [])\n if isinstance(rows, list) and rows:\n logger.reason(\"Parsed JSON from markdown code block\", {\"rows\": len(rows)})\n except json.JSONDecodeError:\n pass # fall through to partial recovery\n else:\n pass # fall through to partial recovery\n \n # If finish_reason=length, try to recover complete rows from truncated JSON\n logger.explore(\"LLM truncated, trying partial row recovery\", extra={\"src\": \"executor\", \"finish_reason\": finish_reason, \"response_length\": len(response_text)})\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n row_data = json.loads(row_text)\n partial_rows.append(row_data)\n except json.JSONDecodeError:\n continue\n if partial_rows:\n logger.explore(f\"Recovered {len(partial_rows)}/{expected_count} complete rows from truncated response\", extra={\"src\": \"executor\"})\n data = {\"rows\": partial_rows}\n else:\n logger.explore(\"Could not recover any complete rows\", extra={\"src\": \"executor\", \"response_preview\": response_text[:1000]})\n raise ValueError(\"LLM response was not valid JSON (could not recover any rows)\")\n else:\n logger.explore(\"No complete rows found in truncated response\", extra={\"src\": \"executor\", \"response_preview\": response_text[:1000]})\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: dict[str, dict[str, str]] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n if not row_id:\n continue\n\n detected_lang = str(item.get(\"detected_source_language\", \"und\")) if item.get(\"detected_source_language\") else \"und\"\n result: dict[str, str] = {\n \"detected_source_language\": detected_lang,\n }\n\n # Multi-language format: extract per-language keys\n has_language_data = False\n if target_languages:\n for lang_code in target_languages:\n lang_val = item.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n result[lang_code] = str(lang_val)\n has_language_data = True\n\n # Fallback to legacy single \"translation\" key format\n if not has_language_data:\n translation = item.get(\"translation\")\n if translation is not None:\n result[\"translation\"] = str(translation)\n has_language_data = True\n\n if has_language_data:\n translations[row_id] = result\n\n if len(translations) < expected_count:\n logger.explore(\n f\"LLM returned fewer translations expected={expected_count} \"\n f\"got={len(translations)}\"\n )\n\n return translations\n # endregion _parse_llm_response\n\n\n# #endregion TranslationExecutor\n# #endregion TranslationExecutor\n" - }, - { - "contract_id": "MAX_RETRIES_PER_BATCH", - "contract_type": "Constant", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 50, - "end_line": 52, - "tier": "TIER_1", - "complexity": 1, - "metadata": {}, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region MAX_RETRIES_PER_BATCH [TYPE Constant]\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\n" - }, - { - "contract_id": "MAX_ROWS_PER_RUN", - "contract_type": "Constant", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 54, - "end_line": 59, - "tier": "TIER_1", - "complexity": 1, - "metadata": {}, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region MAX_ROWS_PER_RUN [TYPE Constant]\n# Safety cap: without it, a datasource with thousands of rows blocks the single\n# uvicorn worker for minutes/hours. Preview uses sample_size (5-10). Full run\n# should stay within reasonable bounds.\nMAX_ROWS_PER_RUN = 10000\n# #endregion MAX_ROWS_PER_RUN\n" - }, - { - "contract_id": "_enforce_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 61, - "end_line": 125, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Post-processing: enforce dictionary entries in LLM output." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region _enforce_dictionary [TYPE Function]\n# @BRIEF Post-processing: enforce dictionary entries in LLM output.\n# If a source term from the dictionary appears in the original text,\n# but the target term is missing from the translation, replace occurrences\n# of the source term in the translated output with the target term.\n# @PRE: dict_matches is a list of dict entries with source_term/target_term.\n# @POST: per_lang_values may be mutated to include forced dictionary replacements.\n# @SIDE_EFFECT: Logs when enforcement is applied.\ndef _enforce_dictionary(\n source_text: str,\n per_lang_values: dict[str, str],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n row_id: str,\n) -> None:\n \"\"\"Post-processing: enforce dictionary terms in LLM translation output.\n\n For each dictionary entry whose source_term appears in the source_text,\n verify that the target_term is present in each language's translation.\n If missing, replace any occurrence of the source term (which the LLM\n may have left untranslated) with the dictionary target term.\n \"\"\"\n if not dict_matches or not source_text:\n return\n\n text_lower = source_text.lower()\n\n for dm in dict_matches:\n src_term = dm.get(\"source_term\", \"\")\n tgt_term = dm.get(\"target_term\", \"\")\n if not src_term or not tgt_term:\n continue\n\n # Only process if source term actually appears in the original text\n if src_term.lower() not in text_lower:\n continue\n\n # Try to replace in each language\n for lang_code in list(per_lang_values.keys()):\n val = per_lang_values[lang_code]\n if not val:\n continue\n\n # Check if target term is already present (case-insensitive)\n if tgt_term.lower() in val.lower():\n continue\n\n # Try to find the source term (or a close variant) in the output\n # This catches cases where the LLM left the source term untranslated\n # Use lambda to prevent regex back-reference injection from tgt_term\n src_pattern = re.compile(re.escape(src_term), re.IGNORECASE)\n if src_pattern.search(val):\n new_val = src_pattern.sub(lambda _: tgt_term, val)\n if new_val != val:\n logger.reason(\"Dictionary enforcement applied\", {\n \"batch_id\": batch_id,\n \"row_id\": row_id,\n \"language_code\": lang_code,\n \"source_term\": src_term,\n \"target_term\": tgt_term,\n \"before\": val[:200],\n \"after\": new_val[:200],\n })\n per_lang_values[lang_code] = new_val\n# #endregion _enforce_dictionary\n" - }, - { - "contract_id": "_compute_source_hash", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 128, - "end_line": 161, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Compute deterministic cache key for a source row." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region _compute_source_hash [TYPE Function]\n# @BRIEF Compute deterministic cache key for a source row.\n# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash).\n# Key columns (row_id, primary keys) are EXCLUDED — only the text content\n# and meaningful context affect the hash. This ensures identical text with\n# different row identifiers still produces a cache hit.\ndef _compute_source_hash(\n source_text: str,\n source_data: dict | None,\n dict_snapshot_hash: str | None,\n config_hash: str | None,\n context_keys: list[str] | None = None,\n) -> str:\n \"\"\"Deterministic cache key for a translation source row.\n\n Only includes source_text and context-relevant fields from source_data.\n Key/identifier columns are excluded so identical text in different rows hits cache.\n \"\"\"\n # Extract only context-relevant fields from source_data (not keys/identifiers)\n context_data: dict[str, str] = {}\n if source_data and context_keys:\n for key in context_keys:\n val = source_data.get(key)\n if val is not None:\n context_data[key] = str(val)\n\n payload = json.dumps({\n \"text\": source_text,\n \"ctx\": context_data, # only context columns, no key/row_id\n \"dict_hash\": dict_snapshot_hash or \"\",\n \"config_hash\": config_hash or \"\",\n }, sort_keys=True, ensure_ascii=False)\n return hashlib.sha256(payload.encode(\"utf-8\")).hexdigest()\n# #endregion _compute_source_hash\n" - }, - { - "contract_id": "_check_translation_cache", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 164, - "end_line": 194, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Look up a previously successful translation by source_hash." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region _check_translation_cache [TYPE Function]\n# @BRIEF Look up a previously successful translation by source_hash.\n# Returns per-language dict {lang_code: final_value} or None.\ndef _check_translation_cache(\n db: Session,\n source_hash: str,\n) -> dict[str, str] | None:\n \"\"\"Check if this source_hash was already translated successfully.\"\"\"\n from sqlalchemy.orm import joinedload\n from ...models.translate import TranslationRecord, TranslationLanguage\n\n cached = (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.source_hash == source_hash,\n TranslationRecord.status == \"SUCCESS\",\n )\n .order_by(TranslationRecord.created_at.desc())\n .first()\n )\n if not cached:\n return None\n\n lang_values: dict[str, str] = {}\n for lang in cached.languages:\n if lang.status == \"translated\" and lang.final_value:\n lang_values[lang.language_code] = lang.final_value\n\n return lang_values if lang_values else None\n# #endregion _check_translation_cache\n" - }, - { - "contract_id": "estimate_row_tokens", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 197, - "end_line": 224, + "end_line": 256, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, - "POST": "Returns estimated token count >= 1.", - "PRE": "source_text is a string.", - "PURPOSE": "Estimate token count for a single source row including context fields." + "PURPOSE": "Process translation in batches: fetch source rows, call LLM, persist TranslationBatch" }, - "relations": [ - { - "source_id": "estimate_row_tokens", - "relation_type": "DEPENDS_ON", - "target_id": "_estimate_tokens_for_text", - "target_ref": "[_estimate_tokens_for_text]" - } - ], + "relations": [], "schema_warnings": [ { "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", "detail": { "actual_complexity": 4, - "contract_type": "Function" + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region estimate_row_tokens [C:4] [TYPE Function] [SEMANTICS translate, token, estimation]\n# @BRIEF Estimate token count for a single source row including context fields.\n# @PRE source_text is a string.\n# @POST Returns estimated token count >= 1.\n# @RELATION DEPENDS_ON -> [_estimate_tokens_for_text]\n\ndef estimate_row_tokens(\n source_text: str,\n source_data: dict | None,\n job,\n) -> int:\n \"\"\"Estimate token count for a single source row including context fields.\n\n Uses CJK-aware heuristics via _token_budget._estimate_tokens_for_text.\n Context fields from job.context_columns are included in the estimate.\n \"\"\"\n from ._token_budget import _estimate_tokens_for_text\n\n text_tokens = _estimate_tokens_for_text(source_text or \"\")\n\n context_keys = job.context_columns or []\n ctx_text = \"\"\n if source_data and context_keys:\n ctx_text = \" \".join(str(source_data.get(k, \"\")) for k in context_keys)\n ctx_tokens = _estimate_tokens_for_text(ctx_text)\n\n return text_tokens + ctx_tokens\n# #endregion estimate_row_tokens\n" + "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", @@ -49335,44 +51793,736 @@ }, { "contract_id": "TranslationOrchestrator", - "contract_type": "Class", + "contract_type": "Module", "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 46, - "end_line": 1137, + "start_line": 1, + "end_line": 176, "tier": "TIER_3", "complexity": 5, "metadata": { "COMPLEXITY": 5, - "INVARIANT": "State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED.", - "POST": "Runs are created, executed, and finalized with event records.", - "PRE": "DB session and config manager are available.", - "PURPOSE": "Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging.", - "SIDE_EFFECT": "Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows." + "DATA_CONTRACT": "Input[db, config_manager, current_user] -> Output[TranslationRun]", + "INVARIANT": "State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run.", + "LAYER": "Domain", + "POST": "Translation run is executed, SQL generated and submitted, events recorded.", + "PRE": "Valid job and accepted preview (for manual runs). Superset and LLM are reachable.", + "PURPOSE": "Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.", + "RATIONALE": "C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary.", + "REJECTED": "stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.", + "SIDE_EFFECT": "Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events." }, - "relations": [], + "relations": [ + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationExecutor", + "target_ref": "[TranslationExecutor]" + }, + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "SQLGenerator", + "target_ref": "[SQLGenerator]" + }, + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetSqlLabExecutor", + "target_ref": "[SupersetSqlLabExecutor]" + }, + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + }, + { + "source_id": "TranslationOrchestrator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewSession", + "target_ref": "[TranslationPreviewSession]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS sqlalchemy, tenacity, translate, orchestration, batch]\n# @BRIEF Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationExecutor]\n# @RELATION DEPENDS_ON -> [SQLGenerator]\n# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @PRE Valid job and accepted preview (for manual runs). Superset and LLM are reachable.\n# @POST Translation run is executed, SQL generated and submitted, events recorded.\n# @SIDE_EFFECT Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events.\n# @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun]\n# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run.\n# @RATIONALE C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary.\n# @REJECTED Distributed actor model (Celery) — eventual-consistency challenges at current scale.\n# @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.\n\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .executor import TranslationExecutor\nfrom .orchestrator_aggregator import TranslationResultAggregator\nfrom .orchestrator_planner import TranslationPlanner\nfrom .orchestrator_runner import TranslationStageRunner\n\n\n# #region TranslationOrchestrator [C:5] [TYPE Class]\n# @BRIEF Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging.\n# @PRE DB session and config manager are available.\n# @POST Runs are created, executed, and finalized with event records.\n# @SIDE_EFFECT Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows.\n# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED.\n# @RATIONALE Delegates to TranslationPlanner, TranslationStageRunner, TranslationResultAggregator for SOLID decomposition.\n# @REJECTED Monolithic class was rejected — violated INV_7 (single contract < 150 lines, module < 400 lines).\nclass TranslationOrchestrator:\n \"\"\"Coordinates full translation run lifecycle via delegated sub-components.\"\"\"\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: str | None = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.event_log = TranslationEventLog(db)\n self._planner = TranslationPlanner(db, self.event_log, current_user)\n self._runner = TranslationStageRunner(db, config_manager, self.event_log, current_user)\n self._aggregator = TranslationResultAggregator(db, self.event_log)\n\n # region start_run [TYPE Function]\n # @PURPOSE: Start a new translation run for a job.\n # @PRE: job_id exists. For manual runs, an accepted preview session must exist.\n # @POST: TranslationRun is created in PENDING status with hash fields and config snapshot.\n # @SIDE_EFFECT: DB writes.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n return self._planner.plan_run(\n job_id=job_id,\n is_scheduled=is_scheduled,\n trigger_type=trigger_type,\n full_translation=full_translation,\n )\n # endregion start_run\n\n # region execute_run [TYPE Function]\n # @PURPOSE: Execute a translation run: dispatch executor, generate SQL, submit to Superset.\n # @PRE: run is in PENDING status.\n # @POST: Run is executed, SQL generated, Superset submission attempted.\n # @SIDE_EFFECT: LLM calls, DB writes, Superset API calls.\n def execute_run(\n self,\n run: TranslationRun,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n skip_insert: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.execute_run\"):\n return self._runner.execute_run(\n run=run,\n on_batch_progress=on_batch_progress,\n skip_insert=skip_insert,\n )\n # endregion execute_run\n\n # region retry_failed_batches [TYPE Function]\n # @PURPOSE: Retry failed batches in a run.\n def retry_failed_batches(self, run_id: str) -> TranslationRun:\n return self._runner.retry_failed_batches(run_id)\n # endregion retry_failed_batches\n\n # region retry_insert [TYPE Function]\n # @PURPOSE: Retry the SQL insert phase for a completed run.\n def retry_insert(self, run_id: str) -> TranslationRun:\n return self._runner.retry_insert(run_id)\n # endregion retry_insert\n\n # region cancel_run [TYPE Function]\n # @PURPOSE: Cancel a running translation.\n def cancel_run(self, run_id: str) -> TranslationRun:\n return self._runner.cancel_run(run_id)\n # endregion cancel_run\n\n # region _generate_and_insert_sql [TYPE Function] [SEMANTICS backward-compat wrapper]\n # @PURPOSE: Backward-compatible delegating wrapper for SQL generation and insert.\n # @SIDE_EFFECT: Delegates to SQLInsertService. May call Superset API.\n def _generate_and_insert_sql(\n self,\n job: Any,\n run: TranslationRun,\n ) -> dict[str, Any]:\n \"\"\"Backward-compatible wrapper — delegates to SQLInsertService.\"\"\"\n from .orchestrator_sql import SQLInsertService\n svc = SQLInsertService(self.db, self.config_manager, self.event_log)\n return svc.generate_and_insert_sql(job, run)\n # endregion _generate_and_insert_sql\n\n # region _update_language_stats [TYPE Function] [SEMANTICS backward-compat wrapper]\n # @PURPOSE: Backward-compatible delegating wrapper for language stats update.\n # @SIDE_EFFECT: Delegates to TranslationResultAggregator.update_language_stats.\n def _update_language_stats(\n self,\n run_id: str,\n language_stats_map: dict[str, Any],\n ) -> None:\n \"\"\"Backward-compatible wrapper — delegates to TranslationResultAggregator.\"\"\"\n return self._aggregator.update_language_stats(run_id, language_stats_map)\n # endregion _update_language_stats\n\n # region get_run_status [TYPE Function]\n # @PURPOSE: Get run status with statistics.\n def get_run_status(self, run_id: str) -> dict[str, Any]:\n return self._aggregator.get_run_status(run_id)\n # endregion get_run_status\n\n # region get_run_records [TYPE Function]\n # @PURPOSE: Get paginated records for a run.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: str | None = None,\n ) -> dict[str, Any]:\n return self._aggregator.get_run_records(run_id, page, page_size, status_filter)\n # endregion get_run_records\n\n # region get_run_history [TYPE Function]\n # @PURPOSE: Get run history for a job.\n def get_run_history(\n self,\n job_id: str,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[dict[str, Any]]]:\n return self._aggregator.get_run_history(job_id, page, page_size)\n # endregion get_run_history\n\n# #endregion TranslationOrchestrator\n# #endregion TranslationOrchestrator\n" + }, + { + "contract_id": "TranslationResultAggregator", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/orchestrator_aggregator.py", + "start_line": 1, + "end_line": 138, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Returns structured run data with pagination.", + "PRE": "Database session is available.", + "PURPOSE": "Query translation run status, records, and history.", + "RATIONALE": "Language stats aggregation extracted to orchestrator_lang_stats; query methods to orchestrator_query." + }, + "relations": [ + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationBatch", + "target_ref": "[TranslationBatch]" + }, + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRunLanguageStats", + "target_ref": "[TranslationRunLanguageStats]" + }, + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + }, + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "orchestrator_lang_stats", + "target_ref": "[orchestrator_lang_stats]" + }, + { + "source_id": "TranslationResultAggregator", + "relation_type": "DEPENDS_ON", + "target_id": "orchestrator_query", + "target_ref": "[orchestrator_query]" + } + ], "schema_warnings": [ { "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C4", "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 4, "contract_type": "Class" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationOrchestrator [C:5] [TYPE Class]\n# @BRIEF Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging.\n# @PRE: DB session and config manager are available.\n# @POST: Runs are created, executed, and finalized with event records.\n# @SIDE_EFFECT: Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows.\n# @INVARIANT: State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED.\nclass TranslationOrchestrator:\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: str | None = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.event_log = TranslationEventLog(db)\n self._job: TranslationJob | None = None\n\n # region start_run [TYPE Function]\n # @PURPOSE: Start a new translation run for a job.\n # @PRE: job_id exists. For manual runs, there must be an accepted preview session.\n # @POST: TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.\n # @SIDE_EFFECT: DB writes.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n # Load and validate job\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n self._job = job\n\n logger.reason(\"Starting translation run\", {\n \"job_id\": job_id,\n \"is_scheduled\": is_scheduled,\n })\n\n # Validate preconditions\n self._validate_preconditions(job, is_scheduled=is_scheduled)\n\n # Compute hashes\n config_hash = self._compute_config_hash(job)\n dict_snapshot_hash = self._compute_dict_snapshot_hash(job_id)\n\n # Build config snapshot\n config_snapshot = {\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"database_dialect\": job.database_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n \"target_schema\": job.target_schema,\n \"target_table\": job.target_table,\n \"source_key_cols\": job.source_key_cols,\n \"target_key_cols\": job.target_key_cols,\n \"translation_column\": job.translation_column,\n \"target_column\": job.target_column,\n \"context_columns\": job.context_columns,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": self._compute_dict_snapshot_hash(job_id),\n \"full_translation\": full_translation,\n }\n\n # Compute key_hash from source_key_cols\n import hashlib\n key_hash_input = json.dumps({\n \"source_key_cols\": job.source_key_cols,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n }, sort_keys=True)\n key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16]\n\n # Create run record with all hash/snapshot fields\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"PENDING\",\n trigger_type=trigger_type or (\"scheduled\" if is_scheduled else \"manual\"),\n config_snapshot=config_snapshot,\n key_hash=key_hash,\n config_hash=config_hash,\n dict_snapshot_hash=dict_snapshot_hash,\n created_by=self.current_user,\n created_at=datetime.now(UTC),\n )\n self.db.add(run)\n self.db.flush()\n\n # Record event\n self.event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash,\n \"dict_snapshot_hash\": dict_snapshot_hash,\n \"key_hash\": key_hash,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n logger.reflect(\"Run created\", {\n \"run_id\": run.id,\n \"job_id\": job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n })\n return run\n # endregion start_run\n\n # region execute_run [TYPE Function]\n # @PURPOSE: Execute a translation run: dispatch executor, generate SQL, submit to Superset.\n # @PRE: run is in PENDING status.\n # @POST: Run is executed, SQL generated, Superset submission attempted.\n # @SIDE_EFFECT: LLM calls, DB writes, Superset API calls.\n def execute_run(\n self,\n run: TranslationRun,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n skip_insert: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n if run.status != \"PENDING\":\n raise ValueError(\n f\"Cannot execute run in status '{run.status}'. \"\n f\"Run must be in PENDING status.\"\n )\n\n logger.reason(\"Executing run\", {\n \"run_id\": run.id,\n \"job_id\": job.id,\n \"skip_insert\": skip_insert,\n })\n\n # Record translation phase start\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"TRANSLATION_PHASE_STARTED\",\n payload={},\n created_by=self.current_user,\n )\n\n # Initialize per-language stats\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 language_stats_map: dict[str, TranslationRunLanguageStats] = {}\n for lang_code in target_languages:\n lang_stat = TranslationRunLanguageStats(\n id=str(uuid.uuid4()),\n run_id=run.id,\n language_code=lang_code,\n total_rows=0,\n translated_rows=0,\n failed_rows=0,\n skipped_rows=0,\n token_count=0,\n estimated_cost=0.0,\n )\n self.db.add(lang_stat)\n language_stats_map[lang_code] = lang_stat\n self.db.flush()\n\n # Dispatch executor\n executor = TranslationExecutor(\n self.db, self.config_manager, self.current_user,\n on_batch_progress=on_batch_progress,\n )\n try:\n run = executor.execute_run(run, llm_progress_callback=None, language_stats_map=language_stats_map)\n except Exception as e:\n logger.explore(\"Translation execution failed\", {\n \"run_id\": run.id,\n \"error\": str(e),\n })\n # Rollback the session if it's in a broken state (e.g. after a\n # failed flush from a previous exception in executor.execute_run).\n # Without this, subsequent flush()/commit() calls raise\n # \"This Session's transaction has been rolled back\".\n try:\n self.db.rollback()\n except Exception:\n pass\n run = self.db.merge(run)\n run.status = \"FAILED\"\n run.error_message = f\"Translation execution failed: {e}\"\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_FAILED\",\n payload={\"error\": str(e), \"phase\": \"translation\"},\n created_by=self.current_user,\n )\n self.db.commit()\n return run\n\n # Check if executor cancelled itself due to cancellation flag\n if run.status == \"CANCELLED\":\n self._update_language_stats(run.id, language_stats_map)\n self.event_log.log_event(\n job_id=job.id if job else (self._job.id if self._job else run.job_id),\n run_id=run.id,\n event_type=\"RUN_CANCELLED\",\n payload={\"reason\": \"cancellation_flag\"},\n created_by=self.current_user,\n )\n self.db.commit()\n logger.reflect(\"Run cancelled via cancellation flag\", {\n \"run_id\": run.id,\n })\n return run\n\n # Aggregate per-language statistics after executor completes\n self._update_language_stats(run.id, language_stats_map)\n\n # Record translation phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"TRANSLATION_PHASE_COMPLETED\",\n payload={\n \"total\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n \"skipped\": run.skipped_records,\n },\n created_by=self.current_user,\n )\n\n # Skip insert phase if requested (e.g., for preview-only execution)\n if skip_insert:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"skip_insert\": True},\n created_by=self.current_user,\n )\n self.db.commit()\n return run\n\n # Generate SQL and submit to Superset\n insert_result = self._generate_and_insert_sql(job, run)\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n run.superset_execution_log = insert_result\n # Preserve the translation-phase status. If the executor already\n # marked the run FAILED (all LLM calls failed) we do NOT upgrade it\n # to COMPLETED just because the insert phase ran.\n if run.status != \"FAILED\":\n run.status = \"COMPLETED\"\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n\n # Record terminal event\n terminal_event = \"RUN_COMPLETED\"\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=terminal_event,\n payload={\n \"insert_status\": insert_result.get(\"status\"),\n \"query_id\": insert_result.get(\"query_id\"),\n \"rows_affected\": insert_result.get(\"rows_affected\"),\n \"total_records\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n \"skipped\": run.skipped_records,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n # Re-query run after commit — refresh may fail if the object was\n # created in a different session or became detached during commit.\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n\n logger.reflect(\"Run execution complete\", {\n \"run_id\": run.id,\n \"status\": run.status,\n \"insert_status\": run.insert_status,\n })\n return run\n # endregion execute_run\n\n # region _generate_and_insert_sql [TYPE Function]\n # @PURPOSE: Generate INSERT SQL from successful records and submit to Superset SQL Lab.\n # @PRE: job has target table configured. run has successful records.\n # @POST: SQL is generated and submitted. Returns execution result.\n # @SIDE_EFFECT: Superset API call.\n def _generate_and_insert_sql(\n self,\n job: TranslationJob,\n run: TranslationRun,\n ) -> dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator._generate_and_insert_sql\"):\n # Fetch successful records with eager-loaded language data\n records = (\n self.db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.run_id == run.id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n logger.reason(\"No successful records to insert\", {\"run_id\": run.id})\n return {\"status\": \"skipped\", \"reason\": \"no_records\", \"query_id\": None}\n\n logger.reason(f\"Generating SQL for {len(records)} records\", {\n \"run_id\": run.id,\n \"dialect\": job.database_dialect or job.target_dialect,\n })\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n\n # Columns that exist in the target ClickHouse table\n columns = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n # Deduplicate while preserving order\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n columns = deduped\n\n # Keys for the context JSON: context_columns + original translation_column\n context_keys = list(job.context_columns or [])\n if job.translation_column and job.translation_column != effective_target and job.translation_column not in context_keys:\n context_keys.append(job.translation_column)\n\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n\n # Detect source language from first TranslationLanguage entry\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n # Build context JSON: all extra columns the user configured\n context_data: dict[str, str] = {}\n for key in context_keys:\n val = source_data.get(key)\n context_data[key] = str(val) if val is not None else \"\"\n\n # ── Shared base row ──\n base_row: dict[str, object] = {}\n\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n # ── 1. ORIGINAL row (is_original = 1) ──\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n # ── 2. TRANSLATION rows (is_original = 0) ──\n # Skip language that matches the source — the original row already covers it\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n # Fallback: no per-language data\n fallback_row = dict(base_row)\n if effective_target:\n fallback_row[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback_row[job.target_language_column] = primary_language\n fallback_row[\"is_original\"] = 0\n rows_for_sql.append(fallback_row)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n # Resolve the real database backend engine from Superset\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(self.config_manager, env_id)\n executor.resolve_database_id(\n target_database_id=job.target_database_id,\n )\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend, falling back to job dialet\", {\n \"error\": str(e),\n })\n real_backend = None\n\n dialect = real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n\n # Generate SQL\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n except ValueError as e:\n logger.explore(\"SQL generation failed\", {\"error\": str(e)})\n return {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n logger.reason(\"SQL generated with dialect\", {\n \"dialect\": dialect,\n \"real_backend\": real_backend,\n \"job_database_dialect\": job.database_dialect,\n \"job_target_dialect\": job.target_dialect,\n })\n\n # Log insert phase start\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_STARTED\",\n payload={\"sql_length\": len(sql), \"row_count\": row_count, \"dialect\": dialect},\n created_by=self.current_user,\n )\n\n # Submit to Superset\n try:\n result = executor.execute_and_poll(\n sql=sql,\n max_polls=30,\n poll_interval_seconds=2.0,\n )\n except Exception as e:\n logger.explore(\"Superset SQL submission failed\", {\n \"run_id\": run.id,\n \"error\": str(e),\n })\n result = {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_COMPLETED\",\n payload=result,\n created_by=self.current_user,\n )\n\n return result\n # endregion _generate_and_insert_sql\n\n # region _validate_preconditions [TYPE Function]\n # @PURPOSE: Validate preconditions before starting a run.\n # @PRE: None.\n # @POST: Raises ValueError if preconditions are not met.\n # @SIDE_EFFECT: None.\n def _validate_preconditions(\n self,\n job: TranslationJob,\n is_scheduled: bool = False,\n ) -> None:\n with belief_scope(\"TranslationOrchestrator._validate_preconditions\"):\n # Job must be in valid status\n if job.status in (\"DRAFT\",):\n raise ValueError(\n f\"Cannot run job '{job.id}' in status '{job.status}'. \"\n f\"Job must be READY, ACTIVE, or COMPLETED.\"\n )\n\n # Must have target table configured\n if not job.target_table:\n raise ValueError(\n f\"Job '{job.id}' has no target table configured. \"\n \"Configure a target table before running.\"\n )\n\n # Must have LLM provider configured\n if not job.provider_id:\n raise ValueError(\n f\"Job '{job.id}' has no LLM provider configured. \"\n \"Select an LLM provider before running.\"\n )\n\n # Must have a translation column\n if not job.translation_column:\n raise ValueError(\n f\"Job '{job.id}' has no translation column configured. \"\n \"Select a translation column before running.\"\n )\n\n # For manual runs, must have accepted preview\n if not is_scheduled:\n accepted_session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job.id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not accepted_session:\n raise ValueError(\n f\"Job '{job.id}' has no accepted preview session. \"\n \"Run and accept a preview before executing a manual translation run.\"\n )\n\n logger.reason(\"Preconditions validated\", {\n \"job_id\": job.id,\n \"is_scheduled\": is_scheduled,\n })\n # endregion _validate_preconditions\n\n # region retry_failed_batches [TYPE Function]\n # @PURPOSE: Retry failed batches in a run.\n # @PRE: run exists and has failed batches.\n # @POST: Failed batches are re-processed.\n # @SIDE_EFFECT: LLM calls, DB writes.\n def retry_failed_batches(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_failed_batches\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n # Find failed batches\n failed_batches = (\n self.db.query(TranslationBatch)\n .filter(\n TranslationBatch.run_id == run_id,\n TranslationBatch.status.in_([\"FAILED\", \"COMPLETED_WITH_ERRORS\"]),\n )\n .all()\n )\n\n if not failed_batches:\n raise ValueError(f\"No failed batches found for run '{run_id}'\")\n\n logger.reason(\"Retrying failed batches\", {\n \"run_id\": run_id,\n \"batch_count\": len(failed_batches),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRYING\",\n payload={\"batch_count\": len(failed_batches)},\n created_by=self.current_user,\n )\n\n # Re-process each failed batch\n executor = TranslationExecutor(self.db, self.config_manager, self.current_user)\n run.status = \"RUNNING\"\n self.db.flush()\n\n for batch in failed_batches:\n # Fetch failed records for this batch\n failed_records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.batch_id == batch.id,\n TranslationRecord.status == \"FAILED\",\n )\n .all()\n )\n\n if not failed_records:\n continue\n\n # Build rows from failed records\n retry_rows = []\n for rec in failed_records:\n retry_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": None,\n \"source_object_name\": rec.source_object_name or \"\",\n })\n\n # Process retry batch\n result = executor._process_batch(\n job=job,\n run_id=run_id,\n batch_index=batch.batch_index,\n batch_rows=retry_rows,\n )\n\n # Update run stats\n run.successful_records = (run.successful_records or 0) + result[\"successful\"]\n run.failed_records = (run.failed_records or 0) + result[\"failed\"]\n run.skipped_records = (run.skipped_records or 0) + result[\"skipped\"]\n self.db.flush()\n\n # Update run status\n if run.failed_records == 0:\n run.status = \"COMPLETED\"\n elif run.successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\"\n\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"retry\": True},\n created_by=self.current_user,\n )\n\n self.db.commit()\n logger.reflect(\"Retry complete\", {\n \"run_id\": run_id,\n \"status\": run.status,\n })\n return run\n # endregion retry_failed_batches\n\n # region retry_insert [TYPE Function]\n # @PURPOSE: Retry the SQL insert phase for a completed run.\n # @PRE: run exists and has successful records.\n # @POST: SQL is regenerated and re-submitted to Superset.\n # @SIDE_EFFECT: Superset API call.\n def retry_insert(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_insert\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n logger.reason(\"Retrying insert phase\", {\n \"run_id\": run_id,\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRY_INSERT\",\n payload={},\n created_by=self.current_user,\n )\n\n # Regenerate SQL and submit\n insert_result = self._generate_and_insert_sql(job, run)\n\n # Update run\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n self.db.flush()\n self.db.commit()\n self.db.refresh(run)\n\n logger.reflect(\"Insert retry complete\", {\n \"run_id\": run_id,\n \"insert_status\": run.insert_status,\n })\n return run\n # endregion retry_insert\n\n # region cancel_run [TYPE Function]\n # @PURPOSE: Cancel a running translation.\n # @PRE: run is in PENDING or RUNNING status.\n # @POST: Run status is set to CANCELLED.\n # @SIDE_EFFECT: DB write; records event.\n def cancel_run(self, run_id: str) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.cancel_run\"):\n # Set short lock timeout to avoid blocking on row lock held by\n # background executor thread (which holds RowExclusiveLock during\n # batch processing). If the row is locked, we fall back to setting\n # a cancellation flag via direct SQL UPDATE, which the executor\n # checks after each batch commit.\n try:\n self.db.execute(text(\"SET LOCAL lock_timeout = '3s'\"))\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n except Exception:\n # Row is locked — set cancellation flag via direct SQL\n logger.explore(\"Row lock timeout — setting cancellation flag via direct SQL\", {\n \"run_id\": run_id,\n })\n self.db.execute(\n text(\"UPDATE translation_runs SET error_message = 'CANCEL_REQUESTED' WHERE id = :run_id\"),\n {\"run_id\": run_id},\n )\n self.db.commit()\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n logger.reflect(\"Cancellation flag set for locked run\", {\n \"run_id\": run_id,\n })\n return run\n\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n if run.status not in (\"PENDING\", \"RUNNING\"):\n raise ValueError(\n f\"Cannot cancel run in status '{run.status}'. \"\n f\"Only PENDING or RUNNING runs can be cancelled.\"\n )\n\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=run.job_id,\n run_id=run.id,\n event_type=\"RUN_CANCELLED\",\n payload={},\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n logger.reflect(\"Run cancelled\", {\n \"run_id\": run_id,\n })\n return run\n # endregion cancel_run\n\n # region get_run_status [TYPE Function]\n # @PURPOSE: Get run status with statistics.\n # @PRE: run_id exists.\n # @POST: Returns dict with run details.\n def get_run_status(self, run_id: str) -> dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator.get_run_status\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n # Count batches\n batch_count = (\n self.db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .count()\n )\n\n # Get event summary\n event_summary = self.event_log.get_run_event_summary(run_id)\n\n # Get language stats\n language_stats_entries = (\n self.db.query(TranslationRunLanguageStats)\n .filter(TranslationRunLanguageStats.run_id == run_id)\n .all()\n )\n language_stats = [\n {\n \"language_code\": ls.language_code,\n \"total_rows\": ls.total_rows or 0,\n \"translated_rows\": ls.translated_rows or 0,\n \"failed_rows\": ls.failed_rows or 0,\n \"skipped_rows\": ls.skipped_rows or 0,\n \"token_count\": ls.token_count or 0,\n \"estimated_cost\": ls.estimated_cost or 0.0,\n }\n for ls in language_stats_entries\n ]\n\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"batch_count\": batch_count,\n \"language_stats\": language_stats,\n \"event_invariants\": {\n \"has_run_started\": event_summary[\"has_run_started\"],\n \"terminal_event_count\": event_summary[\"terminal_event_count\"],\n \"invariant_valid\": event_summary[\"invariant_valid\"],\n },\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n # endregion get_run_status\n\n # region get_run_records [TYPE Function]\n # @PURPOSE: Get paginated records for a run.\n # @PRE: run_id exists.\n # @POST: Returns dict with records and pagination info.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator.get_run_records\"):\n query = (\n self.db.query(TranslationRecord)\n .options(selectinload(TranslationRecord.languages))\n .filter(TranslationRecord.run_id == run_id)\n )\n\n if status_filter:\n query = query.filter(TranslationRecord.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n records = (\n query.order_by(TranslationRecord.created_at.desc())\n .offset(offset)\n .limit(page_size)\n .all()\n )\n\n return {\n \"items\": [\n {\n \"id\": r.id,\n \"batch_id\": r.batch_id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"error_message\": r.error_message,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n \"languages\": [\n {\n \"language_code\": tl.language_code,\n \"translated_value\": tl.translated_value,\n \"final_value\": tl.final_value or tl.translated_value,\n \"source_language_detected\": tl.source_language_detected,\n \"status\": tl.status,\n \"needs_review\": tl.needs_review or False,\n }\n for tl in (r.languages or [])\n ],\n }\n for r in records\n ],\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"status_filter\": status_filter,\n }\n # endregion get_run_records\n\n # region get_run_history [TYPE Function]\n # @PURPOSE: Get run history for a job.\n # @PRE: job_id exists.\n # @POST: Returns list of runs.\n def get_run_history(\n self,\n job_id: str,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[dict[str, Any]]]:\n with belief_scope(\"TranslationOrchestrator.get_run_history\"):\n query = self.db.query(TranslationRun).filter(\n TranslationRun.job_id == job_id\n )\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n return total, [\n {\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in runs\n ]\n # endregion get_run_history\n\n # region _update_language_stats [TYPE Function]\n # @PURPOSE: Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n # @PRE: run_id and language_stats_map are valid. DB session is available.\n # @POST: Language stats are updated with row counts and estimated tokens/cost.\n # @SIDE_EFFECT: DB writes on language_stats objects.\n def _update_language_stats(\n self,\n run_id: str,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n ) -> None:\n with belief_scope(\"TranslationOrchestrator._update_language_stats\"):\n # Get all records for this run to join with TranslationLanguage\n records = (\n self.db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == run_id)\n .all()\n )\n record_ids = [r.id for r in records]\n\n if not record_ids:\n logger.reason(\"No records for language stats aggregation\", {\"run_id\": run_id})\n return\n\n # Get all language entries for this run's records\n lang_entries = (\n self.db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids))\n .all()\n )\n\n # Aggregate by language_code\n from collections import defaultdict\n agg: dict[str, dict[str, int]] = defaultdict(lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n # Estimate tokens: heuristic based on character count of translated values\n total_chars = sum(\n len(le.translated_value or \"\") for le in lang_entries if le.translated_value\n )\n total_tokens = max(1, total_chars // 4) # ~4 chars per token\n cost_per_token = 0.002 / 1000 # $0.002 per 1K tokens\n\n # Update each language stat entry\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n\n # Proportional token split: share tokens across languages\n num_langs = len(language_stats_map)\n if num_langs > 0:\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n\n self.db.flush()\n\n logger.reason(\"Language stats updated\", {\n \"run_id\": run_id,\n \"languages\": list(language_stats_map.keys()),\n \"total_tokens_est\": total_tokens,\n })\n # endregion _update_language_stats\n\n # region _compute_config_hash [TYPE Function]\n # @PURPOSE: Compute a hash of the job's current configuration for snapshot comparison.\n @staticmethod\n def _compute_config_hash(job: TranslationJob) -> str:\n import hashlib\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_languages\": sorted(job.target_languages) if job.target_languages else [],\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n # endregion _compute_config_hash\n\n # region _compute_dict_snapshot_hash [TYPE Function]\n # @PURPOSE: Compute a hash of dictionary state for snapshot comparison.\n def _compute_dict_snapshot_hash(self, job_id: str) -> str:\n import hashlib\n\n from ...models.translate import TranslationJobDictionary\n dict_links = (\n self.db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n # endregion _compute_dict_snapshot_hash\n\n# #endregion TranslationOrchestrator\n" + "body": "# #region TranslationResultAggregator [C:4] [TYPE Class] [SEMANTICS sqlalchemy, translate, query, history, records]\n# @BRIEF Query translation run status, records, and history.\n# @PRE Database session is available.\n# @POST Returns structured run data with pagination.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationRunLanguageStats]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [orchestrator_lang_stats]\n# @RELATION DEPENDS_ON -> [orchestrator_query]\n# @RATIONALE Language stats aggregation extracted to orchestrator_lang_stats; query methods to orchestrator_query.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n TranslationBatch,\n TranslationRun,\n TranslationRunLanguageStats,\n)\nfrom .events import TranslationEventLog\nfrom .orchestrator_lang_stats import update_language_stats\nfrom .orchestrator_query import get_run_history as _get_run_history\nfrom .orchestrator_query import get_run_records as _get_run_records\n\n\nclass TranslationResultAggregator:\n \"\"\"Query run status, records, history, and language statistics.\"\"\"\n\n def __init__(self, db: Session, event_log: TranslationEventLog):\n self.db = db\n self.event_log = event_log\n\n # region get_run_status [TYPE Function]\n # @PURPOSE: Get run status with statistics.\n def get_run_status(self, run_id: str) -> dict[str, Any]:\n with belief_scope(\"TranslationResultAggregator.get_run_status\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n batch_count = (\n self.db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .count()\n )\n event_summary = self.event_log.get_run_event_summary(run_id)\n\n language_stats_entries = (\n self.db.query(TranslationRunLanguageStats)\n .filter(TranslationRunLanguageStats.run_id == run_id)\n .all()\n )\n language_stats = [\n {\n \"language_code\": ls.language_code,\n \"total_rows\": ls.total_rows or 0,\n \"translated_rows\": ls.translated_rows or 0,\n \"failed_rows\": ls.failed_rows or 0,\n \"skipped_rows\": ls.skipped_rows or 0,\n \"token_count\": ls.token_count or 0,\n \"estimated_cost\": ls.estimated_cost or 0.0,\n }\n for ls in language_stats_entries\n ]\n\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"batch_count\": batch_count,\n \"language_stats\": language_stats,\n \"event_invariants\": {\n \"has_run_started\": event_summary[\"has_run_started\"],\n \"terminal_event_count\": event_summary[\"terminal_event_count\"],\n \"invariant_valid\": event_summary[\"invariant_valid\"],\n },\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n # endregion get_run_status\n\n # region get_run_records [TYPE Function]\n # @PURPOSE: Get paginated records for a run.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"TranslationResultAggregator.get_run_records\"):\n return _get_run_records(self.db, run_id, page, page_size, status_filter)\n # endregion get_run_records\n\n # region get_run_history [TYPE Function]\n # @PURPOSE: Get run history for a job.\n def get_run_history(\n self,\n job_id: str,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[dict[str, Any]]]:\n with belief_scope(\"TranslationResultAggregator.get_run_history\"):\n return _get_run_history(self.db, job_id, page, page_size)\n # endregion get_run_history\n\n # region update_language_stats [TYPE Function]\n # @PURPOSE: Aggregate TranslationLanguage entries and update TranslationRunLanguageStats.\n # @SIDE_EFFECT: DB writes on language_stats objects.\n def update_language_stats(\n self,\n run_id: str,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n ) -> None:\n with belief_scope(\"TranslationResultAggregator.update_language_stats\"):\n update_language_stats(\n db=self.db,\n event_log=self.event_log,\n run_id=run_id,\n language_stats_map=language_stats_map,\n )\n # endregion update_language_stats\n\n# #endregion TranslationResultAggregator\n" + }, + { + "contract_id": "orchestrator_cancel", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/orchestrator_cancel.py", + "start_line": 1, + "end_line": 120, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Cancel and retry-insert operations for translation runs.", + "RATIONALE": "Extracted from orchestrator_retry.py for INV_7 compliance (module < 150 lines)." + }, + "relations": [ + { + "source_id": "orchestrator_cancel", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "orchestrator_cancel", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "orchestrator_cancel", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + }, + { + "source_id": "orchestrator_cancel", + "relation_type": "DEPENDS_ON", + "target_id": "SQLInsertService", + "target_ref": "[SQLInsertService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region orchestrator_cancel [C:3] [TYPE Module] [SEMANTICS translate, cancel, retry_insert]\n# @BRIEF Cancel and retry-insert operations for translation runs.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [SQLInsertService]\n# @RATIONALE Extracted from orchestrator_retry.py for INV_7 compliance (module < 150 lines).\n\nfrom datetime import UTC, datetime\n\nfrom sqlalchemy import text\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRun\nfrom .events import TranslationEventLog\nfrom .orchestrator_sql import SQLInsertService\n\n\ndef _fallback_cancel_request(db: Session, run_id: str) -> TranslationRun:\n \"\"\"Set cancellation flag when row lock prevents direct status update.\"\"\"\n try:\n db.rollback()\n except Exception:\n pass\n logger.explore(\"Row lock timeout — setting cancellation flag via direct SQL\", {\"run_id\": run_id})\n db.execute(\n text(\"UPDATE translation_runs SET error_message = 'CANCEL_REQUESTED' WHERE id = :run_id\"),\n {\"run_id\": run_id},\n )\n db.commit()\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n return run\n\n\n# #region cancel_run [C:3] [TYPE Function] [SEMANTICS translate, cancel, run]\n# @BRIEF Cancel a running translation run.\n# @SIDE_EFFECT DB writes; event log.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\ndef cancel_run(\n db: Session,\n event_log: TranslationEventLog,\n current_user: str | None,\n run_id: str,\n) -> TranslationRun:\n \"\"\"Cancel a translation run, handling lock timeout with direct SQL fallback.\"\"\"\n with belief_scope(\"orchestrator_cancel.cancel_run\"):\n try:\n db.execute(text(\"SET LOCAL lock_timeout = '3s'\"))\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n except Exception:\n return _fallback_cancel_request(db, run_id)\n\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n if run.status not in (\"PENDING\", \"RUNNING\"):\n raise ValueError(\n f\"Cannot cancel run in status '{run.status}'. \"\n \"Only PENDING or RUNNING runs can be cancelled.\"\n )\n\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(UTC)\n try:\n db.flush()\n except Exception:\n return _fallback_cancel_request(db, run_id)\n event_log.log_event(\n job_id=run.job_id, run_id=run.id, event_type=\"RUN_CANCELLED\",\n payload={}, created_by=current_user,\n )\n db.commit()\n db.refresh(run)\n return run\n# #endregion cancel_run\n\n\n# #region retry_insert [C:3] [TYPE Function] [SEMANTICS translate, retry, insert]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @SIDE_EFFECT Superset API call; DB writes.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [SQLInsertService]\ndef retry_insert(\n db: Session,\n config_manager: ConfigManager,\n event_log: TranslationEventLog,\n current_user: str | None,\n run_id: str,\n) -> TranslationRun:\n \"\"\"Retry SQL insert for a completed run — generates and submits INSERT statements.\"\"\"\n with belief_scope(\"orchestrator_cancel.retry_insert\"):\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n job = db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_RETRY_INSERT\",\n payload={}, created_by=current_user,\n )\n sql_service = SQLInsertService(db, config_manager, event_log)\n insert_result = sql_service.generate_and_insert_sql(job, run)\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n db.flush()\n db.commit()\n db.refresh(run)\n return run\n# #endregion retry_insert\n# #endregion orchestrator_cancel\n" + }, + { + "contract_id": "orchestrator_config", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/orchestrator_config.py", + "start_line": 1, + "end_line": 47, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Config hash and dictionary snapshot hash utilities for translation planning." + }, + "relations": [ + { + "source_id": "orchestrator_config", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "orchestrator_config", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJobDictionary", + "target_ref": "[TranslationJobDictionary]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region orchestrator_config [C:3] [TYPE Module] [SEMANTICS hash, config, snapshot, translate]\n# @BRIEF Config hash and dictionary snapshot hash utilities for translation planning.\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n\nimport hashlib\nimport json\n\n\n# #region compute_config_hash [C:2] [TYPE Function] [SEMANTICS config, hash, deterministic]\n# @BRIEF Compute a deterministic hash of job configuration for snapshot comparison.\n# @PRE job has valid configuration attributes.\n# @POST Returns 16-char hex SHA-256 hash.\ndef compute_config_hash(job) -> str:\n \"\"\"Compute a hash of the job's current configuration for snapshot comparison.\"\"\"\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_languages\": sorted(job.target_languages) if job.target_languages else [],\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n# #endregion compute_config_hash\n\n\n# #region compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS dictionary, hash, snapshot]\n# @BRIEF Compute a hash of dictionary state for snapshot comparison.\n# @PRE db_session is valid.\n# @POST Returns 16-char hex SHA-256 hash of concatenated dictionary IDs.\ndef compute_dict_snapshot_hash(db_session, job_id: str) -> str:\n \"\"\"Compute a hash of dictionary state for snapshot comparison.\"\"\"\n from ...models.translate import TranslationJobDictionary\n dict_links = (\n db_session.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n# #endregion compute_dict_snapshot_hash\n# #endregion orchestrator_config\n" + }, + { + "contract_id": "TranslationExecutionEngine", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/orchestrator_exec.py", + "start_line": 1, + "end_line": 121, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Run is executed with SQL generated; events recorded.", + "PRE": "Database session and config manager are available. Run is in valid state.", + "PURPOSE": "Execute translation runs: dispatch executor, manage completion and failure paths.", + "SIDE_EFFECT": "LLM calls, DB writes, Superset API calls." + }, + "relations": [ + { + "source_id": "TranslationExecutionEngine", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "TranslationExecutionEngine", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationExecutor", + "target_ref": "[TranslationExecutor]" + }, + { + "source_id": "TranslationExecutionEngine", + "relation_type": "DEPENDS_ON", + "target_id": "SQLInsertService", + "target_ref": "[SQLInsertService]" + }, + { + "source_id": "TranslationExecutionEngine", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationResultAggregator", + "target_ref": "[TranslationResultAggregator]" + }, + { + "source_id": "TranslationExecutionEngine", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + }, + { + "source_id": "TranslationExecutionEngine", + "relation_type": "DEPENDS_ON", + "target_id": "orchestrator_run_completion", + "target_ref": "[orchestrator_run_completion]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TranslationExecutionEngine [C:4] [TYPE Class] [SEMANTICS translate, execution, run, orchestration]\n# @BRIEF Execute translation runs: dispatch executor, manage completion and failure paths.\n# @PRE Database session and config manager are available. Run is in valid state.\n# @POST Run is executed with SQL generated; events recorded.\n# @SIDE_EFFECT LLM calls, DB writes, Superset API calls.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationExecutor]\n# @RELATION DEPENDS_ON -> [SQLInsertService]\n# @RELATION DEPENDS_ON -> [TranslationResultAggregator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [orchestrator_run_completion]\n\nimport uuid\nfrom collections.abc import Callable\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationJob,\n TranslationRun,\n TranslationRunLanguageStats,\n)\nfrom .events import TranslationEventLog\nfrom .executor import TranslationExecutor\nfrom .orchestrator_aggregator import TranslationResultAggregator\nfrom .orchestrator_run_completion import (\n complete_cancelled as _complete_cancelled,\n complete_success as _complete_success,\n handle_executor_failure as _handle_executor_failure,\n)\nfrom .orchestrator_sql import SQLInsertService\n\n\nclass TranslationExecutionEngine:\n \"\"\"Execute translation runs: dispatch executor, handle failure/completion paths.\"\"\"\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n event_log: TranslationEventLog,\n current_user: str | None = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.event_log = event_log\n self.current_user = current_user\n self._sql_service = SQLInsertService(db, config_manager, event_log)\n self._aggregator = TranslationResultAggregator(db, event_log)\n\n # region execute_run [TYPE Function]\n # @PURPOSE: Execute a translation run: dispatch executor, handle outcomes.\n # @PRE: run is in PENDING status.\n # @POST: Run executed, SQL generated, Superset submission attempted.\n # @SIDE_EFFECT: LLM calls, DB writes, Superset API calls.\n def execute_run(\n self,\n run: TranslationRun,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n skip_insert: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationExecutionEngine.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n if run.status != \"PENDING\":\n raise ValueError(\n f\"Cannot execute run in status '{run.status}'. Run must be in PENDING status.\"\n )\n\n logger.reason(\"Executing run\", {\"run_id\": run.id, \"job_id\": job.id, \"skip_insert\": skip_insert})\n self.event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"TRANSLATION_PHASE_STARTED\",\n payload={}, created_by=self.current_user,\n )\n\n language_stats_map = self._init_language_stats(run, job)\n executor = TranslationExecutor(\n self.db, self.config_manager, self.current_user,\n on_batch_progress=on_batch_progress,\n )\n try:\n run = executor.execute_run(run, llm_progress_callback=None, language_stats_map=language_stats_map)\n except Exception as e:\n return _handle_executor_failure(self.db, self.event_log, run, job, e, self.current_user)\n\n if run.status == \"CANCELLED\":\n return _complete_cancelled(self.db, self.event_log, self._aggregator, run, language_stats_map, self.current_user)\n\n return _complete_success(\n self.db, self.event_log, self._aggregator, self._sql_service,\n run, job, skip_insert, language_stats_map, self.current_user,\n )\n # endregion execute_run\n\n # region _init_language_stats [TYPE Function]\n # @PURPOSE: Initialize per-language stats for a run.\n def _init_language_stats(\n self,\n run: TranslationRun,\n job: TranslationJob,\n ) -> dict[str, TranslationRunLanguageStats]:\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 language_stats_map: dict[str, TranslationRunLanguageStats] = {}\n for lang_code in target_languages:\n lang_stat = TranslationRunLanguageStats(\n id=str(uuid.uuid4()), run_id=run.id, language_code=lang_code,\n total_rows=0, translated_rows=0, failed_rows=0, skipped_rows=0,\n token_count=0, estimated_cost=0.0,\n )\n self.db.add(lang_stat)\n language_stats_map[lang_code] = lang_stat\n self.db.flush()\n return language_stats_map\n # endregion _init_language_stats\n\n# #endregion TranslationExecutionEngine\n" + }, + { + "contract_id": "update_language_stats", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/orchestrator_lang_stats.py", + "start_line": 1, + "end_line": 88, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.", + "SIDE_EFFECT": "DB writes on language_stats objects." + }, + "relations": [ + { + "source_id": "update_language_stats", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "update_language_stats", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationLanguage", + "target_ref": "[TranslationLanguage]" + }, + { + "source_id": "update_language_stats", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRunLanguageStats", + "target_ref": "[TranslationRunLanguageStats]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region update_language_stats [C:3] [TYPE Function] [SEMANTICS translate, language, stats, aggregation]\n# @BRIEF Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n# @SIDE_EFFECT DB writes on language_stats objects.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRunLanguageStats]\n\nfrom collections import defaultdict\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger\nfrom ...models.translate import (\n TranslationLanguage,\n TranslationRecord,\n TranslationRunLanguageStats,\n)\nfrom .events import TranslationEventLog\n\n\ndef update_language_stats(\n db: Session,\n event_log: TranslationEventLog | None,\n run_id: str,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n) -> None:\n \"\"\"Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n\n Args:\n db: SQLAlchemy Session.\n event_log: Optional TranslationEventLog for logging events.\n run_id: The run ID to aggregate stats for.\n language_stats_map: Map of language_code -> TranslationRunLanguageStats to update.\n \"\"\"\n records = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == run_id)\n .all()\n )\n record_ids = [r.id for r in records]\n\n if not record_ids:\n logger.reason(\"No records for language stats aggregation\", {\"run_id\": run_id})\n return\n\n lang_entries = (\n db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids))\n .all()\n )\n\n agg: dict[str, dict[str, int]] = defaultdict(\n lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0}\n )\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_chars = sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value)\n total_tokens = max(1, total_chars // 4)\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n num_langs = len(language_stats_map)\n if num_langs > 0:\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n\n db.flush()\n\n logger.reason(\"Language stats updated\", {\n \"run_id\": run_id,\n \"languages\": list(language_stats_map.keys()),\n \"total_tokens_est\": total_tokens,\n })\n# #endregion update_language_stats\n" + }, + { + "contract_id": "TranslationPlanner", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/orchestrator_planner.py", + "start_line": 1, + "end_line": 128, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "TranslationRun is planned with hashes and config snapshot; events recorded.", + "PRE": "Database session and config manager are available.", + "PURPOSE": "Handle translation run planning: validation, config hashing, dictionary snapshot.", + "RATIONALE": "Split from monolithic class: validation -> orchestrator_validation, hashing -> orchestrator_config.", + "SIDE_EFFECT": "Creates TranslationRun; computes config and dict hashes; logs events." + }, + "relations": [ + { + "source_id": "TranslationPlanner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "TranslationPlanner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "TranslationPlanner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + }, + { + "source_id": "TranslationPlanner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewSession", + "target_ref": "[TranslationPreviewSession]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TranslationPlanner [C:4] [TYPE Class] [SEMANTICS sqlalchemy, translate, orchestration, planning]\n# @BRIEF Handle translation run planning: validation, config hashing, dictionary snapshot.\n# @PRE Database session and config manager are available.\n# @POST TranslationRun is planned with hashes and config snapshot; events recorded.\n# @SIDE_EFFECT Creates TranslationRun; computes config and dict hashes; logs events.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RATIONALE Split from monolithic class: validation -> orchestrator_validation, hashing -> orchestrator_config.\n\nimport hashlib\nimport json\nimport uuid\nfrom datetime import UTC, datetime\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationJob,\n TranslationPreviewSession,\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .orchestrator_config import compute_config_hash, compute_dict_snapshot_hash\nfrom .orchestrator_validation import validate_job_preconditions\n\n\nclass TranslationPlanner:\n \"\"\"Plan translation runs: validate preconditions, compute hashes, create run records.\"\"\"\n\n def __init__(self, db: Session, event_log: TranslationEventLog, current_user: str | None = None):\n self.db = db\n self.event_log = event_log\n self.current_user = current_user\n\n # region plan_run [TYPE Function]\n # @PURPOSE: Validate, compute hashes, and create a new TranslationRun.\n # @PRE: job_id exists. For manual runs, an accepted preview session exists.\n # @POST: TranslationRun is created in PENDING status with hash fields and config snapshot.\n # @SIDE_EFFECT: DB writes; event recorded.\n def plan_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationPlanner.plan_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n\n validate_job_preconditions(job, self.db, is_scheduled=is_scheduled)\n\n config_hash_value = compute_config_hash(job)\n dict_hash_value = compute_dict_snapshot_hash(self.db, job_id)\n\n config_snapshot = {\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"database_dialect\": job.database_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n \"target_schema\": job.target_schema,\n \"target_table\": job.target_table,\n \"source_key_cols\": job.source_key_cols,\n \"target_key_cols\": job.target_key_cols,\n \"translation_column\": job.translation_column,\n \"target_column\": job.target_column,\n \"context_columns\": job.context_columns,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": dict_hash_value,\n \"full_translation\": full_translation,\n }\n\n key_hash_input = json.dumps({\n \"source_key_cols\": job.source_key_cols,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n }, sort_keys=True)\n key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16]\n\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"PENDING\",\n trigger_type=trigger_type or (\"scheduled\" if is_scheduled else \"manual\"),\n config_snapshot=config_snapshot,\n key_hash=key_hash,\n config_hash=config_hash_value,\n dict_snapshot_hash=dict_hash_value,\n created_by=self.current_user,\n created_at=datetime.now(UTC),\n )\n self.db.add(run)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash_value,\n \"dict_snapshot_hash\": dict_hash_value,\n \"key_hash\": key_hash,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n logger.reflect(\"Run created\", {\n \"run_id\": run.id,\n \"job_id\": job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n })\n return run\n # endregion plan_run\n\n# #endregion TranslationPlanner\n" + }, + { + "contract_id": "orchestrator_query", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/orchestrator_query.py", + "start_line": 1, + "end_line": 113, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Query translation run records and history with pagination." + }, + "relations": [ + { + "source_id": "orchestrator_query", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "orchestrator_query", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region orchestrator_query [C:3] [TYPE Module] [SEMANTICS translate, query, records, history]\n# @BRIEF Query translation run records and history with pagination.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session, selectinload\n\nfrom ...models.translate import TranslationRecord, TranslationRun\n\n\n# #region get_run_records [C:2] [TYPE Function] [SEMANTICS records, query, paginated]\n# @BRIEF Get paginated records for a run.\ndef get_run_records(\n db: Session,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: str | None = None,\n) -> dict[str, Any]:\n \"\"\"Get paginated records for a run.\"\"\"\n query = (\n db.query(TranslationRecord)\n .options(selectinload(TranslationRecord.languages))\n .filter(TranslationRecord.run_id == run_id)\n )\n if status_filter:\n query = query.filter(TranslationRecord.status == status_filter)\n\n total = query.count()\n offset_val = (page - 1) * page_size\n records = (\n query.order_by(TranslationRecord.created_at.desc())\n .offset(offset_val)\n .limit(page_size)\n .all()\n )\n\n return {\n \"items\": [\n {\n \"id\": r.id,\n \"batch_id\": r.batch_id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"error_message\": r.error_message,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n \"languages\": [\n {\n \"language_code\": tl.language_code,\n \"translated_value\": tl.translated_value,\n \"final_value\": tl.final_value or tl.translated_value,\n \"source_language_detected\": tl.source_language_detected,\n \"status\": tl.status,\n \"needs_review\": tl.needs_review or False,\n }\n for tl in (r.languages or [])\n ],\n }\n for r in records\n ],\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"status_filter\": status_filter,\n }\n# #endregion get_run_records\n\n\n# #region get_run_history [C:2] [TYPE Function] [SEMANTICS history, query, paginated]\n# @BRIEF Get run history for a job with pagination.\ndef get_run_history(\n db: Session,\n job_id: str,\n page: int = 1,\n page_size: int = 20,\n) -> tuple[int, list[dict[str, Any]]]:\n \"\"\"Get run history for a job with pagination.\"\"\"\n query = db.query(TranslationRun).filter(TranslationRun.job_id == job_id)\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n return total, [\n {\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in runs\n ]\n# #endregion get_run_history\n# #endregion orchestrator_query\n" + }, + { + "contract_id": "TranslationRunRetryManager", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/orchestrator_retry.py", + "start_line": 1, + "end_line": 137, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Retried batches are re-executed.", + "PRE": "Database session and config manager are available.", + "PURPOSE": "Manage retry of translation run batches.", + "RATIONALE": "Cancel and retry-insert extracted to orchestrator_cancel module for INV_7 compliance.", + "SIDE_EFFECT": "DB writes, event log entries." + }, + "relations": [ + { + "source_id": "TranslationRunRetryManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "TranslationRunRetryManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationBatch", + "target_ref": "[TranslationBatch]" + }, + { + "source_id": "TranslationRunRetryManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "TranslationRunRetryManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationExecutor", + "target_ref": "[TranslationExecutor]" + }, + { + "source_id": "TranslationRunRetryManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TranslationRunRetryManager [C:4] [TYPE Class] [SEMANTICS translate, retry]\n# @BRIEF Manage retry of translation run batches.\n# @PRE Database session and config manager are available.\n# @POST Retried batches are re-executed.\n# @SIDE_EFFECT DB writes, event log entries.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationExecutor]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RATIONALE Cancel and retry-insert extracted to orchestrator_cancel module for INV_7 compliance.\n\nfrom datetime import UTC, datetime\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationBatch,\n TranslationJob,\n TranslationRecord,\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .executor import TranslationExecutor\nfrom .orchestrator_cancel import cancel_run as _cancel_run\nfrom .orchestrator_cancel import retry_insert as _retry_insert\n\n\nclass TranslationRunRetryManager:\n \"\"\"Manage retry and cancellation of translation runs.\"\"\"\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n event_log: TranslationEventLog,\n current_user: str | None = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.event_log = event_log\n self.current_user = current_user\n\n # region retry_failed_batches [TYPE Function]\n # @PURPOSE: Retry failed batches in a run.\n # @SIDE_EFFECT: Re-executes batch translations; DB writes.\n def retry_failed_batches(self, run_id: str) -> TranslationRun:\n with belief_scope(\"TranslationRunRetryManager.retry_failed_batches\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n\n failed_batches = (\n self.db.query(TranslationBatch)\n .filter(\n TranslationBatch.run_id == run_id,\n TranslationBatch.status.in_([\"FAILED\", \"COMPLETED_WITH_ERRORS\"]),\n )\n .all()\n )\n if not failed_batches:\n raise ValueError(f\"No failed batches found for run '{run_id}'\")\n\n logger.reason(\"Retrying failed batches\", {\"run_id\": run_id, \"batch_count\": len(failed_batches)})\n self.event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_RETRYING\",\n payload={\"batch_count\": len(failed_batches)}, created_by=self.current_user,\n )\n\n executor = TranslationExecutor(self.db, self.config_manager, self.current_user)\n run.status = \"RUNNING\"\n self.db.flush()\n\n for batch in failed_batches:\n failed_records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.batch_id == batch.id,\n TranslationRecord.status == \"FAILED\",\n )\n .all()\n )\n if not failed_records:\n continue\n retry_rows = [\n {\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": None,\n \"source_object_name\": rec.source_object_name or \"\",\n }\n for rec in failed_records\n ]\n result = executor._process_batch(\n job=job, run_id=run_id, batch_index=batch.batch_index,\n batch_rows=retry_rows,\n )\n run.successful_records = (run.successful_records or 0) + result[\"successful\"]\n run.failed_records = (run.failed_records or 0) + result[\"failed\"]\n run.skipped_records = (run.skipped_records or 0) + result[\"skipped\"]\n self.db.flush()\n\n run.status = (\n \"COMPLETED\" if run.failed_records == 0\n else (\"FAILED\" if run.successful_records == 0 else \"COMPLETED\")\n )\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\"retry\": True}, created_by=self.current_user,\n )\n self.db.commit()\n return run\n # endregion retry_failed_batches\n\n # region retry_insert [TYPE Function]\n # @PURPOSE: Retry the SQL insert phase for a completed run.\n # @SIDE_EFFECT: Superset API call; DB writes.\n def retry_insert(self, run_id: str) -> TranslationRun:\n return _retry_insert(self.db, self.config_manager, self.event_log, self.current_user, run_id)\n # endregion retry_insert\n\n # region cancel_run [TYPE Function]\n # @PURPOSE: Cancel a running translation.\n # @SIDE_EFFECT: DB writes; event log.\n def cancel_run(self, run_id: str) -> TranslationRun:\n return _cancel_run(self.db, self.event_log, self.current_user, run_id)\n # endregion cancel_run\n\n# #endregion TranslationRunRetryManager\n" + }, + { + "contract_id": "orchestrator_run_completion", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/orchestrator_run_completion.py", + "start_line": 1, + "end_line": 132, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Post-execution run completion handlers: cancelled, success-with-insert, and failure paths." + }, + "relations": [ + { + "source_id": "orchestrator_run_completion", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRun", + "target_ref": "[TranslationRun]" + }, + { + "source_id": "orchestrator_run_completion", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "orchestrator_run_completion", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationResultAggregator", + "target_ref": "[TranslationResultAggregator]" + }, + { + "source_id": "orchestrator_run_completion", + "relation_type": "DEPENDS_ON", + "target_id": "SQLInsertService", + "target_ref": "[SQLInsertService]" + }, + { + "source_id": "orchestrator_run_completion", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region orchestrator_run_completion [C:3] [TYPE Module] [SEMANTICS translate, run, completion, failure]\n# @BRIEF Post-execution run completion handlers: cancelled, success-with-insert, and failure paths.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationResultAggregator]\n# @RELATION DEPENDS_ON -> [SQLInsertService]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationRunLanguageStats\n\n\n# region handle_executor_failure [TYPE Function]\n# @PURPOSE: Handle executor failure — rollback, mark run as FAILED, log event.\n# @SIDE_EFFECT: DB writes; event log.\ndef handle_executor_failure(\n db,\n event_log,\n run: TranslationRun,\n job: TranslationJob,\n error: Exception,\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Handle executor failure — rollback, mark as FAILED, log event.\"\"\"\n logger.explore(\"Translation execution failed\", {\"run_id\": run.id, \"error\": str(error)})\n try:\n db.rollback()\n except Exception:\n pass\n run = db.merge(run)\n run.status = \"FAILED\"\n run.error_message = f\"Translation execution failed: {error}\"\n run.completed_at = datetime.now(UTC)\n db.flush()\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_FAILED\",\n payload={\"error\": str(error), \"phase\": \"translation\"},\n created_by=current_user,\n )\n db.commit()\n return run\n# endregion handle_executor_failure\n\n\n# region complete_cancelled [TYPE Function]\n# @PURPOSE: Finalize a cancelled run — update language stats and log.\n# @SIDE_EFFECT: DB writes; event log.\ndef complete_cancelled(\n db,\n event_log,\n aggregator,\n run: TranslationRun,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Finalize a cancelled run.\"\"\"\n aggregator.update_language_stats(run.id, language_stats_map)\n event_log.log_event(\n job_id=run.job_id, run_id=run.id, event_type=\"RUN_CANCELLED\",\n payload={\"reason\": \"cancellation_flag\"}, created_by=current_user,\n )\n db.commit()\n return run\n# endregion complete_cancelled\n\n\n# region complete_success [TYPE Function]\n# @PURPOSE: Finalize a successful run — update stats, optionally insert SQL, commit.\n# @SIDE_EFFECT: DB writes; event log; Superset API call if skip_insert is False.\ndef complete_success(\n db,\n event_log,\n aggregator,\n sql_service,\n run: TranslationRun,\n job: TranslationJob,\n skip_insert: bool,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Finalize a successful run with stats update and optional SQL insert.\"\"\"\n aggregator.update_language_stats(run.id, language_stats_map)\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"TRANSLATION_PHASE_COMPLETED\",\n payload={\n \"total\": run.total_records, \"successful\": run.successful_records,\n \"failed\": run.failed_records, \"skipped\": run.skipped_records,\n },\n created_by=current_user,\n )\n\n if skip_insert:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n db.flush()\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\"skip_insert\": True}, created_by=current_user,\n )\n db.commit()\n return run\n\n insert_result = sql_service.generate_and_insert_sql(job, run)\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n run.superset_execution_log = insert_result\n if run.status != \"FAILED\":\n run.status = \"COMPLETED\"\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n run.completed_at = datetime.now(UTC)\n db.flush()\n\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\n \"insert_status\": insert_result.get(\"status\"),\n \"query_id\": insert_result.get(\"query_id\"),\n \"total_records\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n },\n created_by=current_user,\n )\n db.commit()\n run = db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n return run\n# endregion complete_success\n# #endregion orchestrator_run_completion\n" + }, + { + "contract_id": "TranslationStageRunner", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/orchestrator_runner.py", + "start_line": 1, + "end_line": 76, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Run is executed; events recorded.", + "PRE": "Database session and config manager are available. Run is in valid state.", + "PURPOSE": "Coordinate translation run execution, retry, and cancellation via delegated components.", + "SIDE_EFFECT": "LLM calls, DB writes, Superset API calls." + }, + "relations": [ + { + "source_id": "TranslationStageRunner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationExecutionEngine", + "target_ref": "[TranslationExecutionEngine]" + }, + { + "source_id": "TranslationStageRunner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRunRetryManager", + "target_ref": "[TranslationRunRetryManager]" + }, + { + "source_id": "TranslationStageRunner", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TranslationStageRunner [C:4] [TYPE Class] [SEMANTICS sqlalchemy, translate, execution, superset]\n# @BRIEF Coordinate translation run execution, retry, and cancellation via delegated components.\n# @PRE Database session and config manager are available. Run is in valid state.\n# @POST Run is executed; events recorded.\n# @SIDE_EFFECT LLM calls, DB writes, Superset API calls.\n# @RELATION DEPENDS_ON -> [TranslationExecutionEngine]\n# @RELATION DEPENDS_ON -> [TranslationRunRetryManager]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n\nfrom collections.abc import Callable\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope\nfrom ...models.translate import TranslationRun\nfrom .events import TranslationEventLog\nfrom .orchestrator_exec import TranslationExecutionEngine\nfrom .orchestrator_retry import TranslationRunRetryManager\n\n\nclass TranslationStageRunner:\n \"\"\"Coordinate translation run execution, retry, and cancellation via delegated components.\"\"\"\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n event_log: TranslationEventLog,\n current_user: str | None = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.event_log = event_log\n self.current_user = current_user\n self._executor_engine = TranslationExecutionEngine(db, config_manager, event_log, current_user)\n self._retry_mgr = TranslationRunRetryManager(db, config_manager, event_log, current_user)\n\n # region execute_run [TYPE Function]\n # @PURPOSE: Execute a translation run: dispatch executor, generate SQL, submit to Superset.\n # @PRE: run is in PENDING status.\n # @POST: Run is executed, SQL generated, Superset submission attempted.\n # @SIDE_EFFECT: LLM calls, DB writes, Superset API calls.\n def execute_run(\n self,\n run: TranslationRun,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n skip_insert: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationStageRunner.execute_run\"):\n return self._executor_engine.execute_run(\n run=run,\n on_batch_progress=on_batch_progress,\n skip_insert=skip_insert,\n )\n # endregion execute_run\n\n # region retry_failed_batches [TYPE Function]\n # @PURPOSE: Retry failed batches in a run.\n def retry_failed_batches(self, run_id: str) -> TranslationRun:\n return self._retry_mgr.retry_failed_batches(run_id)\n # endregion retry_failed_batches\n\n # region retry_insert [TYPE Function]\n # @PURPOSE: Retry the SQL insert phase for a completed run.\n def retry_insert(self, run_id: str) -> TranslationRun:\n return self._retry_mgr.retry_insert(run_id)\n # endregion retry_insert\n\n # region cancel_run [TYPE Function]\n # @PURPOSE: Cancel a running translation.\n def cancel_run(self, run_id: str) -> TranslationRun:\n return self._retry_mgr.cancel_run(run_id)\n # endregion cancel_run\n\n# #endregion TranslationStageRunner\n" + }, + { + "contract_id": "SQLInsertService", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/orchestrator_sql.py", + "start_line": 1, + "end_line": 118, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "SQL is generated and submitted to Superset; returns execution result.", + "PRE": "Job has target table configured. Run has successful records.", + "PURPOSE": "Generate INSERT SQL from translation records and submit to Superset SQL Lab.", + "RATIONALE": "Row/column building extracted to orchestrator_sql_rows module for INV_7 compliance.", + "SIDE_EFFECT": "Superset API call; event log entries." + }, + "relations": [ + { + "source_id": "SQLInsertService", + "relation_type": "DEPENDS_ON", + "target_id": "SQLGenerator", + "target_ref": "[SQLGenerator]" + }, + { + "source_id": "SQLInsertService", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetSqlLabExecutor", + "target_ref": "[SupersetSqlLabExecutor]" + }, + { + "source_id": "SQLInsertService", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationEventLog", + "target_ref": "[TranslationEventLog]" + }, + { + "source_id": "SQLInsertService", + "relation_type": "DEPENDS_ON", + "target_id": "orchestrator_sql_rows", + "target_ref": "[orchestrator_sql_rows]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region SQLInsertService [C:4] [TYPE Class] [SEMANTICS sql, insert, superset, translate]\n# @BRIEF Generate INSERT SQL from translation records and submit to Superset SQL Lab.\n# @PRE Job has target table configured. Run has successful records.\n# @POST SQL is generated and submitted to Superset; returns execution result.\n# @SIDE_EFFECT Superset API call; event log entries.\n# @RELATION DEPENDS_ON -> [SQLGenerator]\n# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [orchestrator_sql_rows]\n# @RATIONALE Row/column building extracted to orchestrator_sql_rows module for INV_7 compliance.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRecord, TranslationRun\nfrom .events import TranslationEventLog\nfrom .orchestrator_sql_rows import build_columns, build_context_keys, build_rows\nfrom .sql_generator import SQLGenerator\nfrom .superset_executor import SupersetSqlLabExecutor\n\n\nclass SQLInsertService:\n \"\"\"Generate INSERT SQL from successful records and submit to Superset.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager, event_log: TranslationEventLog):\n self.db = db\n self.config_manager = config_manager\n self.event_log = event_log\n\n # region generate_and_insert_sql [TYPE Function]\n # @PURPOSE: Generate INSERT SQL and submit to Superset SQL Lab.\n def generate_and_insert_sql(\n self,\n job: TranslationJob,\n run: TranslationRun,\n ) -> dict[str, Any]:\n with belief_scope(\"SQLInsertService.generate_and_insert_sql\"):\n records = (\n self.db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.run_id == run.id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n logger.reason(\"No successful records to insert\", {\"run_id\": run.id})\n return {\"status\": \"skipped\", \"reason\": \"no_records\", \"query_id\": None}\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n\n columns = build_columns(job, effective_target)\n context_keys = build_context_keys(job, effective_target)\n rows_for_sql = build_rows(records, job, effective_target, primary_language, context_keys)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n dialect = self._resolve_dialect(job)\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n except ValueError as e:\n logger.explore(\"SQL generation failed\", {\"error\": str(e)})\n return {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n self.event_log.log_event(\n job_id=job.id, run_id=run.id,\n event_type=\"INSERT_PHASE_STARTED\",\n payload={\"sql_length\": len(sql), \"row_count\": row_count, \"dialect\": dialect},\n )\n\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(self.config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n result = executor.execute_and_poll(sql=sql, max_polls=30, poll_interval_seconds=2.0)\n except Exception as e:\n logger.explore(\"Superset SQL submission failed\", {\"run_id\": run.id, \"error\": str(e)})\n result = {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n self.event_log.log_event(\n job_id=job.id, run_id=run.id,\n event_type=\"INSERT_PHASE_COMPLETED\",\n payload=result,\n )\n return result\n # endregion generate_and_insert_sql\n\n # region _resolve_dialect [TYPE Function]\n def _resolve_dialect(self, job: TranslationJob) -> str:\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(self.config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend, falling back to job dialect\", {\"error\": str(e)})\n real_backend = None\n return real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n # endregion _resolve_dialect\n\n# #endregion SQLInsertService\n" + }, + { + "contract_id": "orchestrator_sql_rows", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", + "start_line": 1, + "end_line": 116, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Column and row building utilities for SQL insertion in translate plugin." + }, + "relations": [ + { + "source_id": "orchestrator_sql_rows", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "orchestrator_sql_rows", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region orchestrator_sql_rows [C:3] [TYPE Module] [SEMANTICS sql, insert, row, column, builders]\n# @BRIEF Column and row building utilities for SQL insertion in translate plugin.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n\nimport json\nfrom typing import Any\n\nfrom ...models.translate import TranslationJob, TranslationRecord\nfrom .sql_generator import _normalize_timestamp_value\n\n\n# #region build_columns [C:2] [TYPE Function] [SEMANTICS sql, columns, build]\n# @BRIEF Build list of target columns for SQL INSERT from job configuration.\ndef build_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build column list for SQL INSERT from job config.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion build_columns\n\n\n# #region build_context_keys [C:1] [TYPE Function] [SEMANTICS context, keys]\n# @BRIEF Build list of context keys for SQL row data.\ndef build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context key list for SQL row data.\"\"\"\n context_keys = list(job.context_columns or [])\n if job.translation_column and job.translation_column != effective_target and job.translation_column not in context_keys:\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion build_context_keys\n\n\n# #region build_rows [C:2] [TYPE Function] [SEMANTICS sql, rows, build]\n# @BRIEF Build row data for SQL INSERT with per-language expansion.\n# @SIDE_EFFECT: Reads translation record language entries.\ndef build_rows(\n records: list[TranslationRecord],\n job: TranslationJob,\n effective_target: str | None,\n primary_language: str,\n context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row data for SQL INSERT with per-language expansion.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data = {key: str(source_data.get(key, \"\")) for key in context_keys}\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback_row = dict(base_row)\n if effective_target:\n fallback_row[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback_row[job.target_language_column] = primary_language\n fallback_row[\"is_original\"] = 0\n rows_for_sql.append(fallback_row)\n\n return rows_for_sql\n# #endregion build_rows\n# #endregion orchestrator_sql_rows\n" + }, + { + "contract_id": "build_columns", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", + "start_line": 13, + "end_line": 37, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Build list of target columns for SQL INSERT from job configuration." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region build_columns [C:2] [TYPE Function] [SEMANTICS sql, columns, build]\n# @BRIEF Build list of target columns for SQL INSERT from job configuration.\ndef build_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build column list for SQL INSERT from job config.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion build_columns\n" + }, + { + "contract_id": "build_context_keys", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", + "start_line": 40, + "end_line": 48, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Build list of context keys for SQL row data." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region build_context_keys [C:1] [TYPE Function] [SEMANTICS context, keys]\n# @BRIEF Build list of context keys for SQL row data.\ndef build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context key list for SQL row data.\"\"\"\n context_keys = list(job.context_columns or [])\n if job.translation_column and job.translation_column != effective_target and job.translation_column not in context_keys:\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion build_context_keys\n" + }, + { + "contract_id": "build_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", + "start_line": 51, + "end_line": 115, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Build row data for SQL INSERT with per-language expansion.", + "SIDE_EFFECT": "Reads translation record language entries." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region build_rows [C:2] [TYPE Function] [SEMANTICS sql, rows, build]\n# @BRIEF Build row data for SQL INSERT with per-language expansion.\n# @SIDE_EFFECT: Reads translation record language entries.\ndef build_rows(\n records: list[TranslationRecord],\n job: TranslationJob,\n effective_target: str | None,\n primary_language: str,\n context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row data for SQL INSERT with per-language expansion.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data = {key: str(source_data.get(key, \"\")) for key in context_keys}\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback_row = dict(base_row)\n if effective_target:\n fallback_row[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback_row[job.target_language_column] = primary_language\n fallback_row[\"is_original\"] = 0\n rows_for_sql.append(fallback_row)\n\n return rows_for_sql\n# #endregion build_rows\n" + }, + { + "contract_id": "validate_job_preconditions", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/orchestrator_validation.py", + "start_line": 1, + "end_line": 61, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Raises ValueError if any precondition fails.", + "PRE": "Job exists and db session is valid.", + "PURPOSE": "Validate preconditions before starting a translation run." + }, + "relations": [ + { + "source_id": "validate_job_preconditions", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region validate_job_preconditions [C:3] [TYPE Function] [SEMANTICS validation, preconditions, translate]\n# @BRIEF Validate preconditions before starting a translation run.\n# @PRE Job exists and db session is valid.\n# @POST Raises ValueError if any precondition fails.\n# @RELATION DEPENDS_ON -> [TranslationJob]\ndef validate_job_preconditions(\n job,\n db_session,\n is_scheduled: bool = False,\n) -> None:\n \"\"\"Validate preconditions before starting a translation run.\n\n Args:\n job: TranslationJob instance.\n db_session: SQLAlchemy Session for querying preview sessions.\n is_scheduled: Whether this is a scheduled (vs manual) run.\n\n Raises:\n ValueError: If any precondition fails.\n \"\"\"\n if job.status in (\"DRAFT\",):\n raise ValueError(\n f\"Cannot run job '{job.id}' in status '{job.status}'. \"\n f\"Job must be READY, ACTIVE, or COMPLETED.\"\n )\n if not job.target_table:\n raise ValueError(\n f\"Job '{job.id}' has no target table configured. \"\n \"Configure a target table before running.\"\n )\n if not job.provider_id:\n raise ValueError(\n f\"Job '{job.id}' has no LLM provider configured. \"\n \"Select an LLM provider before running.\"\n )\n if not job.translation_column:\n raise ValueError(\n f\"Job '{job.id}' has no translation column configured. \"\n \"Select a translation column before running.\"\n )\n if not is_scheduled:\n # Jobs with a direct Superset datasource can fetch rows from Superset directly\n # (see RunSourceFetcher.fetch_source_rows), so they don't need a preview session.\n # Only require an accepted preview for jobs without a configured datasource.\n if not bool(job.source_datasource_id):\n from ...models.translate import TranslationPreviewSession\n accepted_session = (\n db_session.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job.id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not accepted_session:\n raise ValueError(\n f\"Job '{job.id}' has no accepted preview session. \"\n \"Run and accept a preview before executing a manual translation run.\"\n )\n# #endregion validate_job_preconditions\n" }, { "contract_id": "TranslatePlugin", @@ -49415,42 +52565,659 @@ { "contract_id": "DEFAULT_EXECUTION_PROMPT_TEMPLATE", "contract_type": "Constant", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 43, - "end_line": 64, + "file_path": "backend/src/plugins/translate/preview_constants.py", + "start_line": 1, + "end_line": 23, "tier": "TIER_1", "complexity": 1, - "metadata": {}, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Default LLM prompt template for full execution (batch translation)." + }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Constant'", + "detail": { + "actual_type": "Constant", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Constant' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Constant" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Constant'", + "detail": { + "actual_type": "Constant", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Constant' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Constant" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DEFAULT_EXECUTION_PROMPT_TEMPLATE [TYPE Constant]\n\n# Supports both single-language and multi-language modes via {target_languages} placeholder.\nDEFAULT_EXECUTION_PROMPT_TEMPLATE: str = (\n \"Translate the following database content from {source_language} to the following language(s): {target_languages}.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Target dialect(s): {target_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"IMPORTANT — You MUST use the terminology dictionary above. \"\n \"For any source term listed in the dictionary, you MUST use its exact target translation. \"\n \"Do not translate dictionary terms differently. \"\n \"This is mandatory, not optional.\\n\\n\"\n \"For each row, provide an accurate translation of the text into each target language.\\n\\n\"\n \"Rows to translate:\\n{rows_json}\\n\\n\"\n \"Respond with a JSON object in this exact format:\\n\"\n '{{\"rows\": [{{\"row_id\": \"\", \"detected_source_language\": \"\", \"\": \"\", \"\": \"\"}}]}}\\n'\n \"For each row, return the detected source language as a BCP-47 tag (e.g. 'en', 'ru', 'fr'), or 'und' if uncertain.\\n\"\n \"Include a separate key for EACH target language code with the translated text in that language.\\n\"\n \"Each row_id must match the index provided. Return exactly {row_count} entries.\"\n)\n# #endregion DEFAULT_EXECUTION_PROMPT_TEMPLATE\n" + "body": "# #region DEFAULT_EXECUTION_PROMPT_TEMPLATE [C:1] [TYPE Constant] [SEMANTICS prompt, template]\n# @BRIEF Default LLM prompt template for full execution (batch translation).\n# Supports both single-language and multi-language modes via {target_languages} placeholder.\n\nDEFAULT_EXECUTION_PROMPT_TEMPLATE: str = (\n \"Translate the following database content from {source_language} to the following language(s): {target_languages}.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Target dialect(s): {target_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"IMPORTANT — You MUST use the terminology dictionary above. \"\n \"For any source term listed in the dictionary, you MUST use its exact target translation. \"\n \"Do not translate dictionary terms differently. \"\n \"This is mandatory, not optional.\\n\\n\"\n \"For each row, provide an accurate translation of the text into each target language.\\n\\n\"\n \"Rows to translate:\\n{rows_json}\\n\\n\"\n \"Respond with a JSON object in this exact format:\\n\"\n '{{\"rows\": [{{\"row_id\": \"\", \"detected_source_language\": \"\", \"\": \"\", \"\": \"\"}}]}}\\n'\n \"For each row, return the detected source language as a BCP-47 tag (e.g. 'en', 'ru', 'fr'), or 'und' if uncertain.\\n\"\n \"Include a separate key for EACH target language code with the translated text in that language.\\n\"\n \"Each row_id must match the index provided. Return exactly {row_count} entries.\"\n)\n# #endregion DEFAULT_EXECUTION_PROMPT_TEMPLATE\n" }, { "contract_id": "DEFAULT_PREVIEW_PROMPT_TEMPLATE", "contract_type": "Constant", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 67, - "end_line": 88, + "file_path": "backend/src/plugins/translate/preview_constants.py", + "start_line": 26, + "end_line": 48, "tier": "TIER_1", "complexity": 1, - "metadata": {}, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Default LLM prompt template for preview (sample translation)." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Constant'", + "detail": { + "actual_type": "Constant", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Constant' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Constant" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Constant'", + "detail": { + "actual_type": "Constant", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Constant' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Constant" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region DEFAULT_PREVIEW_PROMPT_TEMPLATE [C:1] [TYPE Constant] [SEMANTICS prompt, template]\n# @BRIEF Default LLM prompt template for preview (sample translation).\n\nDEFAULT_PREVIEW_PROMPT_TEMPLATE: str = (\n \"Translate the following database content.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"IMPORTANT — You MUST use the terminology dictionary above. \"\n \"For any source term listed in the dictionary, you MUST use its exact target translation. \"\n \"Do not translate dictionary terms differently. \"\n \"This is mandatory, not optional.\\n\\n\"\n \"Translate to the following languages: {target_languages}\\n\\n\"\n \"For each row, provide an accurate translation of the '{translation_column}' value into each language.\\n\"\n \"Consider the context columns when determining the meaning of the text.\\n\\n\"\n \"Rows to translate:\\n{rows_json}\\n\\n\"\n \"Respond with a JSON object in this exact format:\\n\"\n '{{\"rows\": [{{\"row_id\": \"\", \"detected_source_language\": \"\", \"language_code_1\": \"\", \"language_code_2\": \"\"}}]}}\\n'\n \"For each row, return the detected source language as a BCP-47 tag (e.g. 'en', 'ru', 'fr'), or 'und' if uncertain.\\n\"\n \"Include a separate key for EACH target language code with the translated text in that language.\\n\"\n \"Each row_id must match the index provided. Return exactly {row_count} entries.\"\n)\n# #endregion DEFAULT_PREVIEW_PROMPT_TEMPLATE\n" + }, + { + "contract_id": "PreviewExecutor", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/preview_executor.py", + "start_line": 1, + "end_line": 149, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Sample rows fetched, LLM called.", + "PRE": "Database session, config manager, and Superset client are available.", + "PURPOSE": "Fetch sample data from Superset and call LLM provider for preview.", + "RATIONALE": "LLM response parsing and hash utilities extracted to preview_response_parser module for INV_7 compliance.", + "SIDE_EFFECT": "Fetches data from Superset; makes HTTP calls to LLM provider." + }, + "relations": [ + { + "source_id": "PreviewExecutor", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetClient", + "target_ref": "[SupersetClient]" + }, + { + "source_id": "PreviewExecutor", + "relation_type": "DEPENDS_ON", + "target_id": "LLMProviderService", + "target_ref": "[LLMProviderService]" + }, + { + "source_id": "PreviewExecutor", + "relation_type": "DEPENDS_ON", + "target_id": "LLMClient", + "target_ref": "[LLMClient]" + }, + { + "source_id": "PreviewExecutor", + "relation_type": "DEPENDS_ON", + "target_id": "preview_response_parser", + "target_ref": "[preview_response_parser]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region PreviewExecutor [C:4] [TYPE Class] [SEMANTICS sqlalchemy, superset, llm, preview]\n# @BRIEF Fetch sample data from Superset and call LLM provider for preview.\n# @PRE Database session, config manager, and Superset client are available.\n# @POST Sample rows fetched, LLM called.\n# @SIDE_EFFECT Fetches data from Superset; makes HTTP calls to LLM provider.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMClient]\n# @RELATION DEPENDS_ON -> [preview_response_parser]\n# @RATIONALE LLM response parsing and hash utilities extracted to preview_response_parser module for INV_7 compliance.\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.translate import TranslationJob\nfrom .preview_llm_client import LLMClient\nfrom .preview_resolve_provider import resolve_provider_model as _resolve_provider_model\nfrom .preview_response_parser import (\n compute_config_hash as _compute_config_hash,\n compute_dict_snapshot_hash as _compute_dict_snapshot_hash,\n extract_data_rows as _extract_data_rows,\n parse_llm_response as _parse_llm_response,\n)\n\n\nclass PreviewExecutor:\n \"\"\"Execute preview operations: fetch sample data from Superset, call LLM, parse responses.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager):\n self.db = db\n self.config_manager = config_manager\n\n # region fetch_sample_rows [TYPE Function]\n # @PURPOSE: Fetch sample rows from Superset dataset for preview.\n # @SIDE_EFFECT: Calls Superset chart data endpoint.\n def fetch_sample_rows(\n self,\n job: TranslationJob,\n sample_size: int = 10,\n env_id: str | None = None,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"PreviewExecutor.fetch_sample_rows\"):\n environments = self.config_manager.get_environments()\n target_env_id = env_id or job.environment_id or job.source_dialect or \"\"\n env_config = next((e for e in environments if e.id == target_env_id), None)\n if not env_config:\n if environments:\n env_config = environments[0]\n else:\n raise ValueError(\"No Superset environments configured\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail,\n template_params={}, effective_filters=[],\n )\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = sample_size\n queries[0].pop(\"result_type\", None)\n queries[0].pop(\"columns\", None)\n queries[0][\"metrics\"] = []\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n try:\n response = client.network.request(\n method=\"POST\", endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n raise ValueError(f\"Failed to fetch sample data from Superset: {e}\")\n\n return _extract_data_rows(response)\n # endregion fetch_sample_rows\n\n # region call_llm [TYPE Function]\n # @PURPOSE: Call the configured LLM provider with a prompt.\n # @SIDE_EFFECT: Makes HTTP call to LLM provider.\n def call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> str:\n with belief_scope(\"PreviewExecutor.call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n from ...services.llm_provider import LLMProviderService\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n disable_reasoning = getattr(job, 'disable_reasoning', False)\n\n if provider_type not in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\", \"litellm\"):\n raise ValueError(f\"Unsupported provider type '{provider_type}' for preview\")\n\n max_attempts = 2\n for attempt in range(max_attempts):\n try:\n response_text = LLMClient.call_openai_compatible(\n base_url=provider.base_url, api_key=api_key, model=model,\n prompt=prompt, provider_type=provider_type,\n max_tokens=max_tokens * (attempt + 1),\n disable_reasoning=disable_reasoning or (attempt > 0),\n )\n break\n except ValueError as e:\n if \"empty content\" in str(e) and attempt < max_attempts - 1:\n logger.explore(\"Empty LLM response, retrying with doubled max_tokens\")\n continue\n raise\n\n return response_text\n # endregion call_llm\n\n # region delegation_helpers [TYPE Function]\n # @PURPOSE: Backward-compatible delegation to preview_response_parser module functions.\n\n @staticmethod\n def parse_llm_response(\n response_text: str, expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n ) -> dict[str, dict[str, str]]:\n return _parse_llm_response(response_text, expected_count, target_languages, finish_reason)\n\n def resolve_provider_model(self, job: TranslationJob) -> str | None:\n return _resolve_provider_model(self.db, job)\n\n @staticmethod\n def compute_config_hash(job: TranslationJob) -> str:\n return _compute_config_hash(job)\n\n def compute_dict_snapshot_hash(self, job_id: str) -> str:\n return _compute_dict_snapshot_hash(self.db, job_id)\n # endregion delegation_helpers\n\n# #endregion PreviewExecutor\n" + }, + { + "contract_id": "PreviewPromptBuilder", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/preview_prompt_builder.py", + "start_line": 1, + "end_line": 133, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Build LLM prompts for preview sessions including dictionary glossary and row context.", + "RATIONALE": "Token estimation and budget helpers extracted to preview_prompt_helpers module for INV_7 compliance." + }, + "relations": [ + { + "source_id": "PreviewPromptBuilder", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryManager", + "target_ref": "[DictionaryManager]" + }, + { + "source_id": "PreviewPromptBuilder", + "relation_type": "DEPENDS_ON", + "target_id": "render_prompt", + "target_ref": "[render_prompt]" + }, + { + "source_id": "PreviewPromptBuilder", + "relation_type": "DEPENDS_ON", + "target_id": "preview_prompt_helpers", + "target_ref": "[preview_prompt_helpers]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region PreviewPromptBuilder [C:3] [TYPE Class] [SEMANTICS prompt, dictionary, preview]\n# @BRIEF Build LLM prompts for preview sessions including dictionary glossary and row context.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [render_prompt]\n# @RELATION DEPENDS_ON -> [preview_prompt_helpers]\n# @RATIONALE Token estimation and budget helpers extracted to preview_prompt_helpers module for INV_7 compliance.\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import TranslationJob\nfrom ...services.llm_prompt_templates import render_prompt\nfrom .dictionary import DictionaryManager\nfrom .preview_constants import DEFAULT_PREVIEW_PROMPT_TEMPLATE\nfrom .preview_prompt_helpers import (\n compute_build_token_metadata,\n estimate_token_budget_for_rows,\n)\n\n\nclass PreviewPromptBuilder:\n \"\"\"Build prompts for preview translation including dictionary glossary and context.\"\"\"\n\n def __init__(self, db: Session):\n self.db = db\n\n # region build_prompt_from_rows [TYPE Function]\n # @PURPOSE: Build the complete LLM prompt from source rows, dictionary, and job config.\n # @PRE: job has valid configuration. source_rows is non-empty.\n # @POST: Returns prompt string and token budget metadata.\n def build_prompt_from_rows(\n self,\n job: TranslationJob,\n source_rows: list[dict[str, Any]],\n sample_size: int,\n prompt_template: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"PreviewPromptBuilder.build_prompt_from_rows\"):\n actual_row_count = len(source_rows)\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 num_languages = len(target_languages)\n\n # Build row metadata\n all_source_texts = []\n row_meta: list[dict[str, Any]] = []\n for idx, row in enumerate(source_rows):\n translation_value = str(row.get(job.translation_column, \"\") or \"\")\n context_values = {}\n if job.context_columns:\n for col in job.context_columns:\n context_values[col] = str(row.get(col, \"\") or \"\")\n all_source_texts.append(translation_value)\n row_meta.append({\n \"row_index\": idx,\n \"source_text\": translation_value,\n \"context_data\": context_values,\n \"source_row\": row,\n })\n\n # Filter dictionary entries with row context\n row_context = row_meta[0].get(\"context_data\") if row_meta else None\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, all_source_texts, job.id,\n row_context=row_context,\n )\n\n dictionary_section = \"\"\n if dict_matches:\n glossary_lines = []\n for match in dict_matches:\n glossary_lines.append(\n f\"- '{match['source_term']}' -> '{match['target_term']}'\"\n f\"{' (' + match['context_notes'] + ')' if match.get('context_notes') else ''}\"\n )\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n rows_json = json.dumps([\n {\"row_id\": str(m[\"row_index\"]), \"text\": m[\"source_text\"], \"context\": m[\"context_data\"]}\n for m in row_meta\n ], indent=2)\n\n target_languages_str = \", \".join(target_languages)\n template = prompt_template or DEFAULT_PREVIEW_PROMPT_TEMPLATE\n prompt = render_prompt(template, {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": target_languages_str,\n \"source_dialect\": job.source_dialect or \"\",\n \"target_languages\": target_languages_str,\n \"translation_column\": job.translation_column or \"\",\n \"dictionary_section\": dictionary_section,\n \"rows_json\": rows_json,\n \"row_count\": str(actual_row_count),\n })\n\n return compute_build_token_metadata(\n prompt=prompt,\n row_meta=row_meta,\n target_languages=target_languages,\n num_languages=num_languages,\n dictionary_section=dictionary_section,\n actual_row_count=actual_row_count,\n sample_size=sample_size,\n )\n # endregion build_prompt_from_rows\n\n # region estimate_token_budget_for_rows [TYPE Function]\n # @PURPOSE: Check token budget and optionally truncate rows.\n def estimate_token_budget_for_rows(\n self,\n source_rows: list[dict[str, Any]],\n target_languages: list[str],\n job: TranslationJob,\n provider_model: str | None,\n ) -> dict[str, Any]:\n with belief_scope(\"PreviewPromptBuilder.estimate_token_budget_for_rows\"):\n return estimate_token_budget_for_rows(\n source_rows=source_rows,\n target_languages=target_languages,\n job=job,\n provider_model=provider_model,\n )\n # endregion estimate_token_budget_for_rows\n\n# #endregion PreviewPromptBuilder\n" + }, + { + "contract_id": "preview_prompt_helpers", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/preview_prompt_helpers.py", + "start_line": 1, + "end_line": 113, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Token estimation metadata and budget helpers for preview prompt building." + }, + "relations": [ + { + "source_id": "preview_prompt_helpers", + "relation_type": "DEPENDS_ON", + "target_id": "estimate_token_budget", + "target_ref": "[estimate_token_budget]" + }, + { + "source_id": "preview_prompt_helpers", + "relation_type": "DEPENDS_ON", + "target_id": "TokenEstimator", + "target_ref": "[TokenEstimator]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region preview_prompt_helpers [C:2] [TYPE Module] [SEMANTICS token, budget, metadata, estimate]\n# @BRIEF Token estimation metadata and budget helpers for preview prompt building.\n# @RELATION DEPENDS_ON -> [estimate_token_budget]\n# @RELATION DEPENDS_ON -> [TokenEstimator]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob\nfrom ._token_budget import DEFAULT_CONTEXT_WINDOW, estimate_token_budget\nfrom .preview_token_estimator import TokenEstimator\n\n\n# #region estimate_token_budget_for_rows [C:2] [TYPE Function] [SEMANTICS token, budget, estimate, truncate]\n# @BRIEF Check token budget and optionally truncate source rows for preview.\n# @POST Returns adjusted source_rows, actual_row_count, and token_budget dict.\ndef estimate_token_budget_for_rows(\n source_rows: list[dict[str, Any]],\n target_languages: list[str],\n job: TranslationJob,\n provider_model: str | None,\n) -> dict[str, Any]:\n \"\"\"Check token budget and optionally truncate rows for preview.\"\"\"\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n adjusted_size = token_budget_dict[\"batch_size_adjusted\"]\n if adjusted_size < actual_row_count:\n logger.explore(\n f\"Reducing preview from {actual_row_count} to {adjusted_size} rows\",\n {\n \"estimated_input\": token_budget_dict[\"estimated_input_tokens\"],\n \"estimated_output\": token_budget_dict[\"estimated_output_tokens\"],\n \"context_window\": DEFAULT_CONTEXT_WINDOW,\n },\n )\n source_rows = source_rows[:adjusted_size]\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n return {\n \"source_rows\": source_rows,\n \"actual_row_count\": actual_row_count,\n \"token_budget\": token_budget_dict,\n }\n# #endregion estimate_token_budget_for_rows\n\n\n# #region compute_build_token_metadata [C:1] [TYPE Function] [SEMANTICS token, metadata, estimate]\n# @BRIEF Compute token estimation metadata for prompt builder return dict.\n# @POST Returns prompt, row_meta, target_languages, cost estimates, and cost_warning.\ndef compute_build_token_metadata(\n prompt: str,\n row_meta: list[dict[str, Any]],\n target_languages: list[str],\n num_languages: int,\n dictionary_section: str,\n actual_row_count: int,\n sample_size: int,\n) -> dict[str, Any]:\n \"\"\"Compute token estimation metadata for the prompt builder return dict.\"\"\"\n sample_prompt_tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n sample_output_tokens = TokenEstimator.estimate_output_tokens(actual_row_count, num_languages)\n sample_total_tokens = sample_prompt_tokens + sample_output_tokens\n sample_cost = TokenEstimator.estimate_cost(sample_total_tokens)\n\n total_est_rows = sample_size * 10\n total_est_tokens = (\n TokenEstimator.estimate_prompt_tokens(\n prompt.replace(str(actual_row_count), str(total_est_rows))\n )\n + TokenEstimator.estimate_output_tokens(total_est_rows, num_languages)\n )\n total_est_cost = TokenEstimator.estimate_cost(total_est_tokens)\n\n cost_warning = TokenEstimator.check_cost_warning(\n sample_size, num_languages, sample_total_tokens, sample_cost,\n )\n\n return {\n \"prompt\": prompt,\n \"row_meta\": row_meta,\n \"target_languages\": target_languages,\n \"num_languages\": num_languages,\n \"dictionary_section\": dictionary_section,\n \"sample_prompt_tokens\": sample_prompt_tokens,\n \"sample_output_tokens\": sample_output_tokens,\n \"sample_total_tokens\": sample_total_tokens,\n \"sample_cost\": sample_cost,\n \"total_est_rows\": total_est_rows,\n \"total_est_tokens\": total_est_tokens,\n \"total_est_cost\": total_est_cost,\n \"cost_warning\": cost_warning,\n \"actual_row_count\": actual_row_count,\n }\n# #endregion compute_build_token_metadata\n# #endregion preview_prompt_helpers\n" + }, + { + "contract_id": "estimate_token_budget_for_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_prompt_helpers.py", + "start_line": 16, + "end_line": 62, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Returns adjusted source_rows, actual_row_count, and token_budget dict.", + "PURPOSE": "Check token budget and optionally truncate source rows for preview." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region estimate_token_budget_for_rows [C:2] [TYPE Function] [SEMANTICS token, budget, estimate, truncate]\n# @BRIEF Check token budget and optionally truncate source rows for preview.\n# @POST Returns adjusted source_rows, actual_row_count, and token_budget dict.\ndef estimate_token_budget_for_rows(\n source_rows: list[dict[str, Any]],\n target_languages: list[str],\n job: TranslationJob,\n provider_model: str | None,\n) -> dict[str, Any]:\n \"\"\"Check token budget and optionally truncate rows for preview.\"\"\"\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n adjusted_size = token_budget_dict[\"batch_size_adjusted\"]\n if adjusted_size < actual_row_count:\n logger.explore(\n f\"Reducing preview from {actual_row_count} to {adjusted_size} rows\",\n {\n \"estimated_input\": token_budget_dict[\"estimated_input_tokens\"],\n \"estimated_output\": token_budget_dict[\"estimated_output_tokens\"],\n \"context_window\": DEFAULT_CONTEXT_WINDOW,\n },\n )\n source_rows = source_rows[:adjusted_size]\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n return {\n \"source_rows\": source_rows,\n \"actual_row_count\": actual_row_count,\n \"token_budget\": token_budget_dict,\n }\n# #endregion estimate_token_budget_for_rows\n" + }, + { + "contract_id": "compute_build_token_metadata", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_prompt_helpers.py", + "start_line": 65, + "end_line": 112, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "POST": "Returns prompt, row_meta, target_languages, cost estimates, and cost_warning.", + "PURPOSE": "Compute token estimation metadata for prompt builder return dict." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region compute_build_token_metadata [C:1] [TYPE Function] [SEMANTICS token, metadata, estimate]\n# @BRIEF Compute token estimation metadata for prompt builder return dict.\n# @POST Returns prompt, row_meta, target_languages, cost estimates, and cost_warning.\ndef compute_build_token_metadata(\n prompt: str,\n row_meta: list[dict[str, Any]],\n target_languages: list[str],\n num_languages: int,\n dictionary_section: str,\n actual_row_count: int,\n sample_size: int,\n) -> dict[str, Any]:\n \"\"\"Compute token estimation metadata for the prompt builder return dict.\"\"\"\n sample_prompt_tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n sample_output_tokens = TokenEstimator.estimate_output_tokens(actual_row_count, num_languages)\n sample_total_tokens = sample_prompt_tokens + sample_output_tokens\n sample_cost = TokenEstimator.estimate_cost(sample_total_tokens)\n\n total_est_rows = sample_size * 10\n total_est_tokens = (\n TokenEstimator.estimate_prompt_tokens(\n prompt.replace(str(actual_row_count), str(total_est_rows))\n )\n + TokenEstimator.estimate_output_tokens(total_est_rows, num_languages)\n )\n total_est_cost = TokenEstimator.estimate_cost(total_est_tokens)\n\n cost_warning = TokenEstimator.check_cost_warning(\n sample_size, num_languages, sample_total_tokens, sample_cost,\n )\n\n return {\n \"prompt\": prompt,\n \"row_meta\": row_meta,\n \"target_languages\": target_languages,\n \"num_languages\": num_languages,\n \"dictionary_section\": dictionary_section,\n \"sample_prompt_tokens\": sample_prompt_tokens,\n \"sample_output_tokens\": sample_output_tokens,\n \"sample_total_tokens\": sample_total_tokens,\n \"sample_cost\": sample_cost,\n \"total_est_rows\": total_est_rows,\n \"total_est_tokens\": total_est_tokens,\n \"total_est_cost\": total_est_cost,\n \"cost_warning\": cost_warning,\n \"actual_row_count\": actual_row_count,\n }\n# #endregion compute_build_token_metadata\n" + }, + { + "contract_id": "preview_response_parser", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/preview_response_parser.py", + "start_line": 1, + "end_line": 137, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Parse LLM JSON responses and Superset data; compute config/dict hashes for preview." + }, + "relations": [ + { + "source_id": "preview_response_parser", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "preview_response_parser", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJobDictionary", + "target_ref": "[TranslationJobDictionary]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region preview_response_parser [C:3] [TYPE Module] [SEMANTICS llm, parse, json, superset, hash]\n# @BRIEF Parse LLM JSON responses and Superset data; compute config/dict hashes for preview.\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n\nimport hashlib\nimport json\nimport re\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.translate import TranslationJob, TranslationJobDictionary\n\n\n# #region parse_llm_response [C:3] [TYPE Function] [SEMANTICS llm, parse, json, translate]\n# @BRIEF Parse LLM JSON response into structured translations dict with per-language support.\n# @PRE response_text is a valid JSON string (possibly wrapped in markdown code block).\n# @POST Returns dict mapping row_id -> {detected_source_language, lang_code: translation, ...}.\ndef parse_llm_response(\n response_text: str,\n expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n) -> dict[str, dict[str, str]]:\n \"\"\"Parse LLM JSON response into structured translations dict.\"\"\"\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n data = None\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n pass\n\n if data is None:\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n partial_rows.append(json.loads(row_text))\n except json.JSONDecodeError:\n continue\n if partial_rows:\n data = {\"rows\": partial_rows}\n if data is None:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: dict[str, dict[str, str]] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n if not row_id:\n continue\n detected_lang = str(item.get(\"detected_source_language\", \"und\")) if item.get(\"detected_source_language\") else \"und\"\n result: dict[str, str] = {\"detected_source_language\": detected_lang}\n has_language_data = False\n if target_languages:\n for lang_code in target_languages:\n lang_val = item.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n result[lang_code] = str(lang_val)\n has_language_data = True\n if not has_language_data:\n translation = item.get(\"translation\")\n if translation is not None:\n result[\"translation\"] = str(translation)\n else:\n continue\n translations[row_id] = result\n\n return translations\n# #endregion parse_llm_response\n\n\n# #region extract_data_rows [C:1] [TYPE Function] [SEMANTICS superset, data, extraction]\n# @BRIEF Extract data rows from Superset chart data API response.\ndef extract_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n \"\"\"Extract data rows from Superset chart data API response.\"\"\"\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, list):\n return result\n return []\n# #endregion extract_data_rows\n\n\n# #region compute_config_hash [C:1] [TYPE Function] [SEMANTICS config, hash, deterministic]\n# @BRIEF Compute a deterministic hash of job configuration for snapshot comparison.\ndef compute_config_hash(job: TranslationJob) -> str:\n \"\"\"Compute a deterministic hash of job configuration for snapshot comparison.\"\"\"\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n# #endregion compute_config_hash\n\n\n# #region compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS dictionary, hash, snapshot]\n# @BRIEF Compute a hash of dictionary state for snapshot comparison.\ndef compute_dict_snapshot_hash(db: Session, job_id: str) -> str:\n \"\"\"Compute a hash of dictionary state for snapshot comparison.\"\"\"\n dict_links = (\n db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n# #endregion compute_dict_snapshot_hash\n# #endregion preview_response_parser\n" + }, + { + "contract_id": "parse_llm_response", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_response_parser.py", + "start_line": 16, + "end_line": 79, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Returns dict mapping row_id -> {detected_source_language, lang_code: translation, ...}.", + "PRE": "response_text is a valid JSON string (possibly wrapped in markdown code block).", + "PURPOSE": "Parse LLM JSON response into structured translations dict with per-language support." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region parse_llm_response [C:3] [TYPE Function] [SEMANTICS llm, parse, json, translate]\n# @BRIEF Parse LLM JSON response into structured translations dict with per-language support.\n# @PRE response_text is a valid JSON string (possibly wrapped in markdown code block).\n# @POST Returns dict mapping row_id -> {detected_source_language, lang_code: translation, ...}.\ndef parse_llm_response(\n response_text: str,\n expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n) -> dict[str, dict[str, str]]:\n \"\"\"Parse LLM JSON response into structured translations dict.\"\"\"\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n data = None\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n pass\n\n if data is None:\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n partial_rows.append(json.loads(row_text))\n except json.JSONDecodeError:\n continue\n if partial_rows:\n data = {\"rows\": partial_rows}\n if data is None:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: dict[str, dict[str, str]] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n if not row_id:\n continue\n detected_lang = str(item.get(\"detected_source_language\", \"und\")) if item.get(\"detected_source_language\") else \"und\"\n result: dict[str, str] = {\"detected_source_language\": detected_lang}\n has_language_data = False\n if target_languages:\n for lang_code in target_languages:\n lang_val = item.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n result[lang_code] = str(lang_val)\n has_language_data = True\n if not has_language_data:\n translation = item.get(\"translation\")\n if translation is not None:\n result[\"translation\"] = str(translation)\n else:\n continue\n translations[row_id] = result\n\n return translations\n# #endregion parse_llm_response\n" + }, + { + "contract_id": "extract_data_rows", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_response_parser.py", + "start_line": 82, + "end_line": 103, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Extract data rows from Superset chart data API response." + }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DEFAULT_PREVIEW_PROMPT_TEMPLATE [TYPE Constant]\n\nDEFAULT_PREVIEW_PROMPT_TEMPLATE: str = (\n \"Translate the following database content.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"IMPORTANT — You MUST use the terminology dictionary above. \"\n \"For any source term listed in the dictionary, you MUST use its exact target translation. \"\n \"Do not translate dictionary terms differently. \"\n \"This is mandatory, not optional.\\n\\n\"\n \"Translate to the following languages: {target_languages}\\n\\n\"\n \"For each row, provide an accurate translation of the '{translation_column}' value into each language.\\n\"\n \"Consider the context columns when determining the meaning of the text.\\n\\n\"\n \"Rows to translate:\\n{rows_json}\\n\\n\"\n \"Respond with a JSON object in this exact format:\\n\"\n '{{\"rows\": [{{\"row_id\": \"\", \"detected_source_language\": \"\", \"language_code_1\": \"\", \"language_code_2\": \"\"}}]}}\\n'\n \"For each row, return the detected source language as a BCP-47 tag (e.g. 'en', 'ru', 'fr'), or 'und' if uncertain.\\n\"\n \"Include a separate key for EACH target language code with the translated text in that language.\\n\"\n \"Each row_id must match the index provided. Return exactly {row_count} entries.\"\n)\n# #endregion DEFAULT_PREVIEW_PROMPT_TEMPLATE\n" + "body": "# #region extract_data_rows [C:1] [TYPE Function] [SEMANTICS superset, data, extraction]\n# @BRIEF Extract data rows from Superset chart data API response.\ndef extract_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n \"\"\"Extract data rows from Superset chart data API response.\"\"\"\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n if isinstance(result, list):\n return result\n return []\n# #endregion extract_data_rows\n" + }, + { + "contract_id": "compute_config_hash", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_response_parser.py", + "start_line": 106, + "end_line": 121, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Compute a deterministic hash of job configuration for snapshot comparison." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region compute_config_hash [C:1] [TYPE Function] [SEMANTICS config, hash, deterministic]\n# @BRIEF Compute a deterministic hash of job configuration for snapshot comparison.\ndef compute_config_hash(job: TranslationJob) -> str:\n \"\"\"Compute a deterministic hash of job configuration for snapshot comparison.\"\"\"\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n# #endregion compute_config_hash\n" + }, + { + "contract_id": "compute_dict_snapshot_hash", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_response_parser.py", + "start_line": 124, + "end_line": 136, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Compute a hash of dictionary state for snapshot comparison." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS dictionary, hash, snapshot]\n# @BRIEF Compute a hash of dictionary state for snapshot comparison.\ndef compute_dict_snapshot_hash(db: Session, job_id: str) -> str:\n \"\"\"Compute a hash of dictionary state for snapshot comparison.\"\"\"\n dict_links = (\n db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n# #endregion compute_dict_snapshot_hash\n" + }, + { + "contract_id": "PreviewSessionManager", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/preview_review.py", + "start_line": 1, + "end_line": 137, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Manage preview session row-level actions: approve/edit/reject rows.", + "RATIONALE": "Session accept/get and serialization extracted to preview_session_ops module for INV_7 compliance." + }, + "relations": [ + { + "source_id": "PreviewSessionManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewSession", + "target_ref": "[TranslationPreviewSession]" + }, + { + "source_id": "PreviewSessionManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewRecord", + "target_ref": "[TranslationPreviewRecord]" + }, + { + "source_id": "PreviewSessionManager", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewLanguage", + "target_ref": "[TranslationPreviewLanguage]" + }, + { + "source_id": "PreviewSessionManager", + "relation_type": "DEPENDS_ON", + "target_id": "preview_session_ops", + "target_ref": "[preview_session_ops]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region PreviewSessionManager [C:3] [TYPE Class] [SEMANTICS preview, session, review, approve]\n# @BRIEF Manage preview session row-level actions: approve/edit/reject rows.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewLanguage]\n# @RELATION DEPENDS_ON -> [preview_session_ops]\n# @RATIONALE Session accept/get and serialization extracted to preview_session_ops module for INV_7 compliance.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationJob,\n TranslationPreviewLanguage,\n TranslationPreviewRecord,\n TranslationPreviewSession,\n)\nfrom .preview_session_ops import (\n accept_preview_session as _accept_preview_session,\n apply_language_action as _apply_language_action,\n apply_record_action as _apply_record_action,\n get_preview_session as _get_preview_session,\n)\n\n\nclass PreviewSessionManager:\n \"\"\"Manage preview session lifecycle: row actions, accept, query.\"\"\"\n\n def __init__(self, db: Session):\n self.db = db\n\n # region update_preview_row [TYPE Function]\n # @PURPOSE: Approve, edit, or reject an individual preview row (optionally per language).\n def update_preview_row(\n self,\n job_id: str,\n row_id: str,\n action: str,\n translation: str | None = None,\n feedback: str | None = None,\n language_code: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.update_preview_row\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"ACTIVE\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No active preview session for job '{job_id}'\")\n\n record = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.id == row_id,\n TranslationPreviewRecord.session_id == session.id,\n )\n .first()\n )\n if not record:\n raise ValueError(f\"Preview record '{row_id}' not found in active session\")\n\n if language_code:\n lang_entry = (\n self.db.query(TranslationPreviewLanguage)\n .filter(\n TranslationPreviewLanguage.preview_record_id == row_id,\n TranslationPreviewLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(\n f\"Language entry '{language_code}' not found for record '{row_id}'\"\n )\n _apply_language_action(lang_entry, action, translation)\n\n all_langs = (\n self.db.query(TranslationPreviewLanguage)\n .filter(TranslationPreviewLanguage.preview_record_id == row_id)\n .all()\n )\n if all(le.status in (\"approved\", \"edited\") for le in all_langs):\n record.status = \"APPROVED\"\n if action == \"edit\" and translation is not None and all_langs:\n record.target_sql = all_langs[0].final_value or all_langs[0].translated_value\n else:\n _apply_record_action(record, action, translation)\n\n if feedback is not None:\n record.feedback = feedback\n\n self.db.commit()\n self.db.refresh(record)\n\n return {\n \"id\": record.id,\n \"source_sql\": record.source_sql,\n \"target_sql\": record.target_sql,\n \"status\": record.status,\n \"feedback\": record.feedback,\n \"languages\": [\n {\n \"language_code\": le.language_code,\n \"source_language_detected\": le.source_language_detected,\n \"translated_value\": le.translated_value,\n \"final_value\": le.final_value,\n \"user_edit\": le.user_edit,\n \"status\": le.status,\n \"needs_review\": le.needs_review,\n }\n for le in (record.languages or [])\n ],\n }\n # endregion update_preview_row\n\n # region accept_preview_session [TYPE Function]\n # @PURPOSE: Mark a preview session as accepted, which gates full execution.\n def accept_preview_session(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.accept_preview_session\"):\n return _accept_preview_session(self.db, job_id)\n # endregion accept_preview_session\n\n # region get_preview_session [TYPE Function]\n # @PURPOSE: Get the latest preview session for a job with its records.\n def get_preview_session(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.get_preview_session\"):\n return _get_preview_session(self.db, job_id)\n # endregion get_preview_session\n\n# #endregion PreviewSessionManager\n" + }, + { + "contract_id": "preview_session_ops", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/preview_session_ops.py", + "start_line": 1, + "end_line": 94, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Preview session lifecycle operations: accept and query preview sessions." + }, + "relations": [ + { + "source_id": "preview_session_ops", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewSession", + "target_ref": "[TranslationPreviewSession]" + }, + { + "source_id": "preview_session_ops", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewRecord", + "target_ref": "[TranslationPreviewRecord]" + }, + { + "source_id": "preview_session_ops", + "relation_type": "DEPENDS_ON", + "target_id": "preview_session_serializer", + "target_ref": "[preview_session_serializer]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region preview_session_ops [C:3] [TYPE Module] [SEMANTICS preview, session, accept, get]\n# @BRIEF Preview session lifecycle operations: accept and query preview sessions.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n# @RELATION DEPENDS_ON -> [preview_session_serializer]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.translate import (\n TranslationJob,\n TranslationPreviewRecord,\n TranslationPreviewSession,\n)\nfrom .preview_session_serializer import (\n apply_language_action, # noqa: F401 — re-export for preview_review.py\n apply_record_action, # noqa: F401 — re-export for preview_review.py\n serialize_preview_record as _serialize_preview_record,\n)\n\n\n# #region accept_preview_session [C:2] [TYPE Function] [SEMANTICS preview, session, accept]\n# @BRIEF Mark a preview session as accepted, which gates full execution.\n# @SIDE_EFFECT: DB writes on session status.\ndef accept_preview_session(db: Session, job_id: str, _current_user: str | None = None) -> dict[str, Any]:\n \"\"\"Mark a preview session as accepted and return the session data with records.\"\"\"\n session = (\n db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id, TranslationPreviewSession.status == \"ACTIVE\")\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No active preview session for job '{job_id}'\")\n session.status = \"APPLIED\"\n db.commit()\n db.refresh(session)\n\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id)\n .all()\n )\n job = db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n target_languages = job.target_languages or [job.target_dialect or \"en\"] if job else [\"en\"]\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n return {\n \"id\": session.id,\n \"job_id\": job_id,\n \"status\": \"APPLIED\",\n \"created_by\": session.created_by,\n \"created_at\": session.created_at.isoformat(),\n \"expires_at\": session.expires_at.isoformat() if session.expires_at else None,\n \"target_languages\": target_languages,\n \"records\": [_serialize_preview_record(r) for r in records],\n }\n# #endregion accept_preview_session\n\n\n# #region get_preview_session [C:2] [TYPE Function] [SEMANTICS preview, session, query]\n# @BRIEF Get the latest preview session for a job with its records.\ndef get_preview_session(db: Session, job_id: str) -> dict[str, Any]:\n \"\"\"Get the latest preview session for a job with its records.\"\"\"\n session = (\n db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id)\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No preview session found for job '{job_id}'\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id)\n .all()\n )\n job = db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n target_languages = job.target_languages or [job.target_dialect or \"en\"] if job else [\"en\"]\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n return {\n \"id\": session.id,\n \"job_id\": job_id,\n \"status\": session.status,\n \"created_by\": session.created_by,\n \"created_at\": session.created_at.isoformat(),\n \"expires_at\": session.expires_at.isoformat() if session.expires_at else None,\n \"target_languages\": target_languages,\n \"records\": [_serialize_preview_record(r) for r in records],\n }\n# #endregion get_preview_session\n# #endregion preview_session_ops\n" + }, + { + "contract_id": "get_preview_session", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_session_ops.py", + "start_line": 62, + "end_line": 93, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Get the latest preview session for a job with its records." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region get_preview_session [C:2] [TYPE Function] [SEMANTICS preview, session, query]\n# @BRIEF Get the latest preview session for a job with its records.\ndef get_preview_session(db: Session, job_id: str) -> dict[str, Any]:\n \"\"\"Get the latest preview session for a job with its records.\"\"\"\n session = (\n db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id)\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No preview session found for job '{job_id}'\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id)\n .all()\n )\n job = db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n target_languages = job.target_languages or [job.target_dialect or \"en\"] if job else [\"en\"]\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n return {\n \"id\": session.id,\n \"job_id\": job_id,\n \"status\": session.status,\n \"created_by\": session.created_by,\n \"created_at\": session.created_at.isoformat(),\n \"expires_at\": session.expires_at.isoformat() if session.expires_at else None,\n \"target_languages\": target_languages,\n \"records\": [_serialize_preview_record(r) for r in records],\n }\n# #endregion get_preview_session\n" + }, + { + "contract_id": "preview_session_serializer", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/preview_session_serializer.py", + "start_line": 1, + "end_line": 101, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Serialization and action helpers for preview sessions: record serialization and per-row approve/reject/edit actions.", + "RATIONALE": "Extracted from preview_session_ops.py for INV_7 compliance (module < 150 lines)." + }, + "relations": [ + { + "source_id": "preview_session_serializer", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewRecord", + "target_ref": "[TranslationPreviewRecord]" + }, + { + "source_id": "preview_session_serializer", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationPreviewLanguage", + "target_ref": "[TranslationPreviewLanguage]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region preview_session_serializer [C:3] [TYPE Module] [SEMANTICS preview, record, serialize, action]\n# @BRIEF Serialization and action helpers for preview sessions: record serialization and per-row approve/reject/edit actions.\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewLanguage]\n# @RATIONALE Extracted from preview_session_ops.py for INV_7 compliance (module < 150 lines).\n\nfrom typing import Any\n\nfrom ...models.translate import TranslationPreviewLanguage, TranslationPreviewRecord\n\n\n# #region serialize_preview_record [C:1] [TYPE Function] [SEMANTICS preview, record, serialize]\n# @BRIEF Serialize a TranslationPreviewRecord to a response dict.\ndef serialize_preview_record(r: TranslationPreviewRecord) -> dict[str, Any]:\n \"\"\"Serialize a TranslationPreviewRecord to a response dict.\"\"\"\n return {\n \"id\": r.id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"feedback\": r.feedback,\n \"source_language_detected\": (\n r.languages[0].source_language_detected\n if r.languages and r.languages[0].source_language_detected\n else None\n ),\n \"needs_review\": r.languages[0].needs_review if r.languages else False,\n \"languages\": [\n {\n \"language_code\": le.language_code,\n \"source_language_detected\": le.source_language_detected,\n \"translated_value\": le.translated_value,\n \"final_value\": le.final_value,\n \"user_edit\": le.user_edit,\n \"status\": le.status,\n \"needs_review\": le.needs_review,\n }\n for le in (r.languages or [])\n ] if r.languages else [],\n }\n# #endregion serialize_preview_record\n\n\n# #region apply_language_action [C:2] [TYPE Function] [SEMANTICS preview, language, action]\n# @BRIEF Apply approve/reject/edit action to a TranslationPreviewLanguage entry.\ndef apply_language_action(\n lang_entry: TranslationPreviewLanguage,\n action: str,\n translation: str | None,\n) -> None:\n \"\"\"Apply action to a per-language preview entry.\"\"\"\n if action == \"approve\":\n lang_entry.status = \"approved\"\n elif action == \"reject\":\n lang_entry.status = \"rejected\"\n elif action == \"edit\":\n lang_entry.status = \"edited\"\n if translation is not None:\n lang_entry.translated_value = translation\n lang_entry.final_value = translation\n lang_entry.user_edit = translation\n else:\n raise ValueError(f\"Invalid action '{action}'. Use 'approve', 'reject', or 'edit'.\")\n# #endregion apply_language_action\n\n\n# #region apply_record_action [C:2] [TYPE Function] [SEMANTICS preview, record, action]\n# @BRIEF Apply approve/reject/edit action to a TranslationPreviewRecord and its language entries.\ndef apply_record_action(\n record: TranslationPreviewRecord,\n action: str,\n translation: str | None,\n) -> None:\n \"\"\"Apply action to a preview record.\"\"\"\n if action == \"approve\":\n record.status = \"APPROVED\"\n for lang_entry in (record.languages or []):\n if lang_entry.status == \"pending\":\n lang_entry.status = \"approved\"\n elif action == \"reject\":\n record.status = \"REJECTED\"\n for lang_entry in (record.languages or []):\n if lang_entry.status == \"pending\":\n lang_entry.status = \"rejected\"\n elif action == \"edit\":\n record.status = \"APPROVED\"\n if translation is not None:\n record.target_sql = translation\n if record.languages:\n first_lang = record.languages[0]\n first_lang.translated_value = translation\n first_lang.final_value = translation\n first_lang.user_edit = translation\n first_lang.status = \"edited\"\n else:\n raise ValueError(f\"Invalid action '{action}'. Use 'approve', 'reject', or 'edit'.\")\n# #endregion apply_record_action\n# #endregion preview_session_serializer\n" + }, + { + "contract_id": "serialize_preview_record", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_session_serializer.py", + "start_line": 12, + "end_line": 44, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Serialize a TranslationPreviewRecord to a response dict." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region serialize_preview_record [C:1] [TYPE Function] [SEMANTICS preview, record, serialize]\n# @BRIEF Serialize a TranslationPreviewRecord to a response dict.\ndef serialize_preview_record(r: TranslationPreviewRecord) -> dict[str, Any]:\n \"\"\"Serialize a TranslationPreviewRecord to a response dict.\"\"\"\n return {\n \"id\": r.id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"feedback\": r.feedback,\n \"source_language_detected\": (\n r.languages[0].source_language_detected\n if r.languages and r.languages[0].source_language_detected\n else None\n ),\n \"needs_review\": r.languages[0].needs_review if r.languages else False,\n \"languages\": [\n {\n \"language_code\": le.language_code,\n \"source_language_detected\": le.source_language_detected,\n \"translated_value\": le.translated_value,\n \"final_value\": le.final_value,\n \"user_edit\": le.user_edit,\n \"status\": le.status,\n \"needs_review\": le.needs_review,\n }\n for le in (r.languages or [])\n ] if r.languages else [],\n }\n# #endregion serialize_preview_record\n" + }, + { + "contract_id": "apply_language_action", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_session_serializer.py", + "start_line": 47, + "end_line": 67, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Apply approve/reject/edit action to a TranslationPreviewLanguage entry." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region apply_language_action [C:2] [TYPE Function] [SEMANTICS preview, language, action]\n# @BRIEF Apply approve/reject/edit action to a TranslationPreviewLanguage entry.\ndef apply_language_action(\n lang_entry: TranslationPreviewLanguage,\n action: str,\n translation: str | None,\n) -> None:\n \"\"\"Apply action to a per-language preview entry.\"\"\"\n if action == \"approve\":\n lang_entry.status = \"approved\"\n elif action == \"reject\":\n lang_entry.status = \"rejected\"\n elif action == \"edit\":\n lang_entry.status = \"edited\"\n if translation is not None:\n lang_entry.translated_value = translation\n lang_entry.final_value = translation\n lang_entry.user_edit = translation\n else:\n raise ValueError(f\"Invalid action '{action}'. Use 'approve', 'reject', or 'edit'.\")\n# #endregion apply_language_action\n" + }, + { + "contract_id": "apply_record_action", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_session_serializer.py", + "start_line": 70, + "end_line": 100, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Apply approve/reject/edit action to a TranslationPreviewRecord and its language entries." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region apply_record_action [C:2] [TYPE Function] [SEMANTICS preview, record, action]\n# @BRIEF Apply approve/reject/edit action to a TranslationPreviewRecord and its language entries.\ndef apply_record_action(\n record: TranslationPreviewRecord,\n action: str,\n translation: str | None,\n) -> None:\n \"\"\"Apply action to a preview record.\"\"\"\n if action == \"approve\":\n record.status = \"APPROVED\"\n for lang_entry in (record.languages or []):\n if lang_entry.status == \"pending\":\n lang_entry.status = \"approved\"\n elif action == \"reject\":\n record.status = \"REJECTED\"\n for lang_entry in (record.languages or []):\n if lang_entry.status == \"pending\":\n lang_entry.status = \"rejected\"\n elif action == \"edit\":\n record.status = \"APPROVED\"\n if translation is not None:\n record.target_sql = translation\n if record.languages:\n first_lang = record.languages[0]\n first_lang.translated_value = translation\n first_lang.final_value = translation\n first_lang.user_edit = translation\n first_lang.status = \"edited\"\n else:\n raise ValueError(f\"Invalid action '{action}'. Use 'approve', 'reject', or 'edit'.\")\n# #endregion apply_record_action\n" }, { "contract_id": "TokenEstimator", "contract_type": "Class", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 91, - "end_line": 149, + "file_path": "backend/src/plugins/translate/preview_token_estimator.py", + "start_line": 1, + "end_line": 51, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { + "COMPLEXITY": 2, "PURPOSE": "Estimate token counts and costs for LLM translation operations.", "RATIONALE": "Token estimation uses a heuristic (chars/token ratio) since exact tokenization depends on the LLM model.", "REJECTED": "Using an external tokenizer library would introduce a heavy dependency for estimation only." @@ -49459,7 +53226,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TokenEstimator [TYPE Class]\n# @BRIEF Estimate token counts and costs for LLM translation operations.\n# @RATIONALE: Token estimation uses a heuristic (chars/token ratio) since exact tokenization depends on the LLM model.\n# @REJECTED: Using an external tokenizer library would introduce a heavy dependency for estimation only.\nclass TokenEstimator:\n \"\"\"Estimate token counts and costs for LLM operations.\"\"\"\n\n CHARS_PER_TOKEN_ESTIMATE: float = 2.2\n OUTPUT_TOKENS_PER_ROW_ESTIMATE: int = 120\n MULTI_LANG_FACTOR: float = 1.2 # Overhead for multi-language in one call\n TOKEN_COST_PER_1K: float = 0.002 # Default cost per 1K tokens\n COST_WARNING_THRESHOLD: int = 30 # Show warning above this sample size\n\n # region estimate_prompt_tokens [TYPE Function]\n # @PURPOSE: Estimate token count for a prompt string.\n # @PRE: prompt is a non-empty string.\n # @POST: Returns estimated token count (integer).\n @staticmethod\n def estimate_prompt_tokens(prompt: str) -> int:\n if not prompt:\n return 0\n return max(1, int(len(prompt) / TokenEstimator.CHARS_PER_TOKEN_ESTIMATE))\n # endregion estimate_prompt_tokens\n\n # region estimate_output_tokens [TYPE Function]\n # @PURPOSE: Estimate output token count for translating N rows across N languages.\n # @PRE: row_count >= 0, num_languages >= 1.\n # @POST: Returns estimated output token count.\n @staticmethod\n def estimate_output_tokens(row_count: int, num_languages: int = 1) -> int:\n return int(row_count * num_languages * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE * TokenEstimator.MULTI_LANG_FACTOR)\n # endregion estimate_output_tokens\n\n # region estimate_cost [TYPE Function]\n # @PURPOSE: Estimate cost for a given number of tokens.\n # @PRE: total_tokens >= 0.\n # @POST: Returns estimated cost in USD.\n @staticmethod\n def estimate_cost(total_tokens: int, cost_per_1k: float | None = None) -> float:\n rate = cost_per_1k if cost_per_1k is not None else TokenEstimator.TOKEN_COST_PER_1K\n return round((total_tokens / 1000) * rate, 6)\n # endregion estimate_cost\n\n # region check_cost_warning [TYPE Function]\n # @PURPOSE: Generate cost warning for large previews.\n # @PRE: sample_size > 0, num_languages >= 1.\n # @POST: Returns warning string or None.\n @staticmethod\n def check_cost_warning(sample_size: int, num_languages: int, estimated_tokens: int, estimated_cost: float) -> str | None:\n if sample_size > TokenEstimator.COST_WARNING_THRESHOLD:\n return (\n f\"Large preview — estimated {estimated_tokens} tokens, ~${estimated_cost:.4f} cost \"\n f\"(across {num_languages} languages)\"\n )\n return None\n # endregion check_cost_warning\n\n\n# #endregion TokenEstimator\n" + "body": "# #region TokenEstimator [C:2] [TYPE Class] [SEMANTICS tokens, cost, estimation]\n# @BRIEF Estimate token counts and costs for LLM translation operations.\n# @RATIONALE Token estimation uses a heuristic (chars/token ratio) since exact tokenization depends on the LLM model.\n# @REJECTED Using an external tokenizer library would introduce a heavy dependency for estimation only.\n\nclass TokenEstimator:\n \"\"\"Estimate token counts and costs for LLM operations.\"\"\"\n\n CHARS_PER_TOKEN_ESTIMATE: float = 2.2\n OUTPUT_TOKENS_PER_ROW_ESTIMATE: int = 120\n MULTI_LANG_FACTOR: float = 1.2\n TOKEN_COST_PER_1K: float = 0.002\n COST_WARNING_THRESHOLD: int = 30\n\n # region estimate_prompt_tokens [TYPE Function]\n # @PURPOSE: Estimate token count for a prompt string.\n @staticmethod\n def estimate_prompt_tokens(prompt: str) -> int:\n if not prompt:\n return 0\n return max(1, int(len(prompt) / TokenEstimator.CHARS_PER_TOKEN_ESTIMATE))\n # endregion estimate_prompt_tokens\n\n # region estimate_output_tokens [TYPE Function]\n # @PURPOSE: Estimate output token count for translating N rows across N languages.\n @staticmethod\n def estimate_output_tokens(row_count: int, num_languages: int = 1) -> int:\n return int(row_count * num_languages * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE * TokenEstimator.MULTI_LANG_FACTOR)\n # endregion estimate_output_tokens\n\n # region estimate_cost [TYPE Function]\n # @PURPOSE: Estimate cost for a given number of tokens.\n @staticmethod\n def estimate_cost(total_tokens: int, cost_per_1k: float | None = None) -> float:\n rate = cost_per_1k if cost_per_1k is not None else TokenEstimator.TOKEN_COST_PER_1K\n return round((total_tokens / 1000) * rate, 6)\n # endregion estimate_cost\n\n # region check_cost_warning [TYPE Function]\n # @PURPOSE: Generate cost warning for large previews.\n @staticmethod\n def check_cost_warning(sample_size: int, num_languages: int, estimated_tokens: int, estimated_cost: float) -> str | None:\n if sample_size > TokenEstimator.COST_WARNING_THRESHOLD:\n return (\n f\"Large preview — estimated {estimated_tokens} tokens, ~${estimated_cost:.4f} cost \"\n f\"(across {num_languages} languages)\"\n )\n return None\n # endregion check_cost_warning\n\n# #endregion TokenEstimator\n" }, { "contract_id": "ContextAwarePromptBuilder", @@ -49504,7 +53271,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/scheduler.py", "start_line": 1, - "end_line": 436, + "end_line": 443, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -49546,14 +53313,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationScheduler [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, schedule, cron, job]\n# @BRIEF Manage TranslationSchedule rows and register them with core SchedulerService.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationSchedule:Class]\n# @RELATION DEPENDS_ON -> [SchedulerService:Class]\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator:Class]\n# @RELATION DEPENDS_ON -> [TranslationEventLog:Class]\n# @PRE: Database session and SchedulerService are available.\n# @POST: TranslationSchedule CRUD persisted; APScheduler jobs registered/updated/removed.\n# @SIDE_EFFECT: Registers APScheduler jobs; runs translations on trigger; creates events.\n# @RATIONALE: Uses existing SchedulerService (APScheduler) to avoid creating a second scheduler instance.\n# @REJECTED: Separate scheduler instance would create resource contention.\n# @REJECTED: Polling-based approach — event-driven APScheduler is more precise.\n\nimport uuid\nfrom datetime import UTC, datetime, timedelta\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.cot_logger import seed_trace_id\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationSchedule\nfrom ...services.notifications.service import NotificationService\nfrom .events import TranslationEventLog\n\n\n# #region TranslationScheduler [C:4] [TYPE Class]\n# @BRIEF CRUD for TranslationSchedule rows + APScheduler registration wrappers.\nclass TranslationScheduler:\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 self.event_log = TranslationEventLog(db)\n\n # region create_schedule [TYPE Function]\n # @PURPOSE: Create a new schedule for a job.\n # @PRE: job_id exists. cron_expression is valid.\n # @POST: TranslationSchedule row created.\n def create_schedule(\n self,\n job_id: str,\n cron_expression: str,\n timezone: str = \"UTC\",\n is_active: bool = True,\n execution_mode: str = \"full\",\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.create_schedule\"):\n # Verify job exists\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n\n schedule = TranslationSchedule(\n id=str(uuid.uuid4()),\n job_id=job_id,\n cron_expression=cron_expression,\n timezone=timezone,\n is_active=is_active,\n execution_mode=execution_mode,\n created_by=self.current_user,\n )\n self.db.add(schedule)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_CREATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": cron_expression,\n \"timezone\": timezone,\n },\n created_by=self.current_user,\n )\n\n logger.reflect(\"Schedule created\", {\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # endregion create_schedule\n\n # region update_schedule [TYPE Function]\n # @PURPOSE: Update an existing schedule.\n # @PRE: job_id has an existing schedule.\n # @POST: Schedule updated.\n def update_schedule(\n self,\n job_id: str,\n cron_expression: str | None = None,\n timezone_str: str | None = None,\n is_active: bool | None = None,\n execution_mode: str | None = None,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.update_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n if cron_expression is not None:\n schedule.cron_expression = cron_expression\n if timezone_str is not None:\n schedule.timezone = timezone_str\n if is_active is not None:\n schedule.is_active = is_active\n if execution_mode is not None:\n schedule.execution_mode = execution_mode\n schedule.updated_at = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_UPDATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n },\n created_by=self.current_user,\n )\n\n logger.reflect(\"Schedule updated\", {\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # endregion update_schedule\n\n # region delete_schedule [TYPE Function]\n # @PURPOSE: Delete a schedule for a job.\n # @PRE: job_id has an existing schedule.\n # @POST: Schedule deleted.\n def delete_schedule(self, job_id: str) -> None:\n with belief_scope(\"TranslationScheduler.delete_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule_id = schedule.id\n self.db.delete(schedule)\n self.db.commit()\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_DELETED\",\n payload={\"schedule_id\": schedule_id},\n created_by=self.current_user,\n )\n\n logger.reflect(\"Schedule deleted\", {\"schedule_id\": schedule_id, \"job_id\": job_id})\n # endregion delete_schedule\n\n # region enable_disable_schedule [TYPE Function]\n # @PURPOSE: Enable or disable a schedule.\n # @PRE: job_id has an existing schedule.\n # @POST: Schedule is_active updated.\n def set_schedule_active(self, job_id: str, is_active: bool) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.set_schedule_active\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(schedule)\n\n logger.reflect(\"Schedule active state set\", {\n \"schedule_id\": schedule.id,\n \"job_id\": job_id,\n \"is_active\": is_active,\n })\n return schedule\n # endregion enable_disable_schedule\n\n # region get_schedule [TYPE Function]\n # @PURPOSE: Get schedule for a job.\n # @PRE: job_id exists.\n # @POST: Returns TranslationSchedule or raises ValueError.\n def get_schedule(self, job_id: str) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.get_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n return schedule\n # endregion get_schedule\n\n # region list_active_schedules [TYPE Function]\n # @PURPOSE: List all active schedules.\n # @POST: Returns list of active TranslationSchedule rows.\n @staticmethod\n def list_active_schedules(db: Session) -> list[TranslationSchedule]:\n return (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active)\n .all()\n )\n # endregion list_active_schedules\n\n # region get_next_executions [TYPE Function]\n # @PURPOSE: Compute next N execution times from cron expression.\n # @PRE: cron_expression is valid.\n # @POST: Returns list of ISO datetime strings.\n @staticmethod\n def get_next_executions(cron_expression: str, timezone_str: str = \"UTC\", n: int = 3) -> list[str]:\n from zoneinfo import ZoneInfo\n\n from apscheduler.triggers.cron import CronTrigger\n\n try:\n tz = ZoneInfo(timezone_str)\n trigger = CronTrigger.from_crontab(cron_expression, timezone=tz)\n except (ValueError, KeyError) as e:\n logger.warning(f\"[get_next_executions] Invalid cron: {e}\")\n return []\n\n now = datetime.now(tz)\n results = []\n next_time = now\n prev = None\n for _ in range(n):\n ft = trigger.get_next_fire_time(prev, next_time)\n if ft is None:\n break\n results.append(ft.isoformat())\n prev = ft\n next_time = ft\n return results\n # endregion get_next_executions\n\n\n# #endregion TranslationScheduler\n\n\n# #region execute_scheduled_translation [C:4] [TYPE Function]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and new-key-only fallback.\n# @PRE schedule_id is valid.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\n# @COMPLEXITY 4\n\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n execution_mode: str = \"full\",\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n seed_trace_id()\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n logger.info(f\"[scheduled_translation] Schedule inactive/missing: {schedule_id}\")\n return\n\n logger.reason(\"Scheduled translation triggered\", {\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n stale_threshold = timedelta(hours=1)\n now = datetime.now(UTC)\n\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .order_by(TranslationRun.created_at.asc()) # oldest first\n .first()\n )\n if active_run:\n is_stale = (\n active_run.status == \"PENDING\"\n and active_run.created_at\n and (now - active_run.created_at) > stale_threshold\n )\n if is_stale:\n # Mark ALL stale PENDING runs as FAILED\n stale_runs = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status == \"PENDING\",\n TranslationRun.created_at < (now - stale_threshold),\n )\n .all()\n )\n for sr in stale_runs:\n sr.status = \"FAILED\"\n sr.error_message = \"Stale: concurrency deadlock — marked by scheduled trigger\"\n sr.completed_at = now\n db.commit()\n logger.explore(f\"Cleared {len(stale_runs)} stale PENDING run(s), proceeding with scheduled trigger\", {\n \"job_id\": job_id,\n \"stale_run_ids\": [sr.id for sr in stale_runs],\n })\n # Proceed to execute the scheduled run (do NOT return)\n else:\n logger.explore(\"Skipping scheduled run — concurrent run in progress\", {\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(UTC) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n logger.reason(\"Baseline expired — full translation\", {\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n\n # Determine trigger type based on execution_mode and baseline expiry\n if execution_mode == \"new_key_only\" and not baseline_expired:\n run.trigger_type = \"new_key_only\"\n elif execution_mode == \"new_key_only\" and baseline_expired:\n logger.reason(\"Baseline expired — falling back to full translation\", {\n \"job_id\": job_id, \"schedule_id\": schedule_id, \"age_days\": age.days,\n })\n run.trigger_type = \"scheduled\"\n else:\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n logger.explore(\"Scheduled translation execution failed\", {\n \"run_id\": run.id,\n \"error\": str(exec_err),\n })\n # Send notification on scheduled-run failure (FR-041, FR-048)\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation failed: job={job_id}\"\n body = (\n f\"Scheduled translation run failed for job '{job_id}'.\\n\"\n f\"Schedule: {schedule_id}\\n\"\n f\"Error: {exec_err}\\n\"\n f\"Time: {datetime.now(UTC).isoformat()}\"\n )\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception as notify_err:\n logger.warning(f\"Failed to send failure notification: {notify_err}\")\n\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(UTC)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(UTC)\n db.commit()\n\n logger.reflect(\"Scheduled translation complete\", {\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n logger.error(f\"[scheduled_translation] Unexpected error: {e}\")\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation CRASHED: job={job_id}\"\n body = f\"Scheduled translation for job '{job_id}' crashed unexpectedly.\\nError: {e}\\nTime: {datetime.now(UTC).isoformat()}\"\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception:\n pass\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n# #endregion TranslationScheduler\n" + "body": "# #region TranslationScheduler [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, schedule, cron, job]\n# @BRIEF Manage TranslationSchedule rows and register them with core SchedulerService.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationSchedule:Class]\n# @RELATION DEPENDS_ON -> [SchedulerService:Class]\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator:Class]\n# @RELATION DEPENDS_ON -> [TranslationEventLog:Class]\n# @PRE: Database session and SchedulerService are available.\n# @POST: TranslationSchedule CRUD persisted; APScheduler jobs registered/updated/removed.\n# @SIDE_EFFECT: Registers APScheduler jobs; runs translations on trigger; creates events.\n# @RATIONALE: Uses existing SchedulerService (APScheduler) to avoid creating a second scheduler instance.\n# @REJECTED: Separate scheduler instance would create resource contention.\n# @REJECTED: Polling-based approach — event-driven APScheduler is more precise.\n\nimport uuid\nfrom datetime import UTC, datetime, timedelta\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.cot_logger import seed_trace_id\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationSchedule\nfrom ...services.notifications.service import NotificationService\nfrom .events import TranslationEventLog\n\n\n# #region TranslationScheduler [C:4] [TYPE Class]\n# @BRIEF CRUD for TranslationSchedule rows + APScheduler registration wrappers.\nclass TranslationScheduler:\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 self.event_log = TranslationEventLog(db)\n\n # region create_schedule [TYPE Function]\n # @PURPOSE: Create a new schedule for a job.\n # @PRE: job_id exists. cron_expression is valid.\n # @POST: TranslationSchedule row created.\n def create_schedule(\n self,\n job_id: str,\n cron_expression: str,\n timezone: str = \"UTC\",\n is_active: bool = True,\n execution_mode: str = \"full\",\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.create_schedule\"):\n # Verify job exists\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n\n schedule = TranslationSchedule(\n id=str(uuid.uuid4()),\n job_id=job_id,\n cron_expression=cron_expression,\n timezone=timezone,\n is_active=is_active,\n execution_mode=execution_mode,\n created_by=self.current_user,\n )\n self.db.add(schedule)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_CREATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": cron_expression,\n \"timezone\": timezone,\n },\n created_by=self.current_user,\n )\n\n logger.reflect(\"Schedule created\", {\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # endregion create_schedule\n\n # region update_schedule [TYPE Function]\n # @PURPOSE: Update an existing schedule.\n # @PRE: job_id has an existing schedule.\n # @POST: Schedule updated.\n def update_schedule(\n self,\n job_id: str,\n cron_expression: str | None = None,\n timezone_str: str | None = None,\n is_active: bool | None = None,\n execution_mode: str | None = None,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.update_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n if cron_expression is not None:\n schedule.cron_expression = cron_expression\n if timezone_str is not None:\n schedule.timezone = timezone_str\n if is_active is not None:\n schedule.is_active = is_active\n if execution_mode is not None:\n schedule.execution_mode = execution_mode\n schedule.updated_at = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_UPDATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n },\n created_by=self.current_user,\n )\n\n logger.reflect(\"Schedule updated\", {\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # endregion update_schedule\n\n # region delete_schedule [TYPE Function]\n # @PURPOSE: Delete a schedule for a job.\n # @PRE: job_id has an existing schedule.\n # @POST: Schedule deleted.\n def delete_schedule(self, job_id: str) -> None:\n with belief_scope(\"TranslationScheduler.delete_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule_id = schedule.id\n self.db.delete(schedule)\n self.db.commit()\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_DELETED\",\n payload={\"schedule_id\": schedule_id},\n created_by=self.current_user,\n )\n\n logger.reflect(\"Schedule deleted\", {\"schedule_id\": schedule_id, \"job_id\": job_id})\n # endregion delete_schedule\n\n # region enable_disable_schedule [TYPE Function]\n # @PURPOSE: Enable or disable a schedule.\n # @PRE: job_id has an existing schedule.\n # @POST: Schedule is_active updated.\n def set_schedule_active(self, job_id: str, is_active: bool) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.set_schedule_active\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(schedule)\n\n logger.reflect(\"Schedule active state set\", {\n \"schedule_id\": schedule.id,\n \"job_id\": job_id,\n \"is_active\": is_active,\n })\n return schedule\n # endregion enable_disable_schedule\n\n # region get_schedule [TYPE Function]\n # @PURPOSE: Get schedule for a job.\n # @PRE: job_id exists.\n # @POST: Returns TranslationSchedule or raises ValueError.\n def get_schedule(self, job_id: str) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.get_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n return schedule\n # endregion get_schedule\n\n # region list_active_schedules [TYPE Function]\n # @PURPOSE: List all active schedules.\n # @POST: Returns list of active TranslationSchedule rows.\n @staticmethod\n def list_active_schedules(db: Session) -> list[TranslationSchedule]:\n return (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active)\n .all()\n )\n # endregion list_active_schedules\n\n # region get_next_executions [TYPE Function]\n # @PURPOSE: Compute next N execution times from cron expression.\n # @PRE: cron_expression is valid.\n # @POST: Returns list of ISO datetime strings.\n @staticmethod\n def get_next_executions(cron_expression: str, timezone_str: str = \"UTC\", n: int = 3) -> list[str]:\n from zoneinfo import ZoneInfo\n\n from apscheduler.triggers.cron import CronTrigger\n\n try:\n tz = ZoneInfo(timezone_str)\n trigger = CronTrigger.from_crontab(cron_expression, timezone=tz)\n except (ValueError, KeyError) as e:\n logger.warning(f\"[get_next_executions] Invalid cron: {e}\")\n return []\n\n now = datetime.now(tz)\n results = []\n next_time = now\n prev = None\n for _ in range(n):\n ft = trigger.get_next_fire_time(prev, next_time)\n if ft is None:\n break\n results.append(ft.isoformat())\n prev = ft\n next_time = ft\n return results\n # endregion get_next_executions\n\n\n# #endregion TranslationScheduler\n\n\n# #region execute_scheduled_translation [C:4] [TYPE Function]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and new-key-only fallback.\n# @PRE schedule_id is valid.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\n# @COMPLEXITY 4\n\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n execution_mode: str = \"full\",\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n seed_trace_id()\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n logger.info(f\"[scheduled_translation] Schedule inactive/missing: {schedule_id}\")\n return\n\n logger.reason(\"Scheduled translation triggered\", {\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n stale_threshold = timedelta(hours=1)\n now = datetime.now(UTC)\n\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .order_by(TranslationRun.created_at.asc()) # oldest first\n .first()\n )\n if active_run:\n is_stale = (\n active_run.status == \"PENDING\"\n and active_run.created_at\n and (now - active_run.created_at) > stale_threshold\n )\n if is_stale:\n # Mark ALL stale PENDING runs as FAILED\n stale_runs = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status == \"PENDING\",\n TranslationRun.created_at < (now - stale_threshold),\n )\n .all()\n )\n for sr in stale_runs:\n sr.status = \"FAILED\"\n sr.error_message = \"Stale: concurrency deadlock — marked by scheduled trigger\"\n sr.completed_at = now\n db.commit()\n logger.explore(f\"Cleared {len(stale_runs)} stale PENDING run(s), proceeding with scheduled trigger\", {\n \"job_id\": job_id,\n \"stale_run_ids\": [sr.id for sr in stale_runs],\n })\n # Proceed to execute the scheduled run (do NOT return)\n else:\n logger.explore(\"Skipping scheduled run — concurrent run in progress\", {\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(UTC) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n logger.reason(\"Baseline expired — full translation\", {\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n\n # Determine trigger type based on execution_mode and baseline expiry\n if execution_mode == \"new_key_only\" and not baseline_expired:\n run.trigger_type = \"new_key_only\"\n elif execution_mode == \"new_key_only\" and baseline_expired:\n logger.reason(\"Baseline expired — falling back to full translation\", {\n \"job_id\": job_id, \"schedule_id\": schedule_id, \"age_days\": age.days,\n })\n run.trigger_type = \"scheduled\"\n else:\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n if run.status == \"FAILED\":\n logger.explore(\"Scheduled translation completed with all records failed\", {\n \"run_id\": run.id,\n \"error\": run.error_message,\n })\n run.insert_status = run.insert_status or None\n else:\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n logger.explore(\"Scheduled translation execution failed\", {\n \"run_id\": run.id,\n \"error\": str(exec_err),\n })\n # Send notification on scheduled-run failure (FR-041, FR-048)\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation failed: job={job_id}\"\n body = (\n f\"Scheduled translation run failed for job '{job_id}'.\\n\"\n f\"Schedule: {schedule_id}\\n\"\n f\"Error: {exec_err}\\n\"\n f\"Time: {datetime.now(UTC).isoformat()}\"\n )\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception as notify_err:\n logger.warning(f\"Failed to send failure notification: {notify_err}\")\n\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(UTC)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(UTC)\n db.commit()\n\n logger.reflect(\"Scheduled translation complete\", {\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n logger.error(f\"[scheduled_translation] Unexpected error: {e}\")\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation CRASHED: job={job_id}\"\n body = f\"Scheduled translation for job '{job_id}' crashed unexpectedly.\\nError: {e}\\nTime: {datetime.now(UTC).isoformat()}\"\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception:\n pass\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n# #endregion TranslationScheduler\n" }, { "contract_id": "execute_scheduled_translation", "contract_type": "Function", "file_path": "backend/src/plugins/translate/scheduler.py", "start_line": 243, - "end_line": 435, + "end_line": 442, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -49578,59 +53345,160 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region execute_scheduled_translation [C:4] [TYPE Function]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and new-key-only fallback.\n# @PRE schedule_id is valid.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\n# @COMPLEXITY 4\n\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n execution_mode: str = \"full\",\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n seed_trace_id()\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n logger.info(f\"[scheduled_translation] Schedule inactive/missing: {schedule_id}\")\n return\n\n logger.reason(\"Scheduled translation triggered\", {\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n stale_threshold = timedelta(hours=1)\n now = datetime.now(UTC)\n\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .order_by(TranslationRun.created_at.asc()) # oldest first\n .first()\n )\n if active_run:\n is_stale = (\n active_run.status == \"PENDING\"\n and active_run.created_at\n and (now - active_run.created_at) > stale_threshold\n )\n if is_stale:\n # Mark ALL stale PENDING runs as FAILED\n stale_runs = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status == \"PENDING\",\n TranslationRun.created_at < (now - stale_threshold),\n )\n .all()\n )\n for sr in stale_runs:\n sr.status = \"FAILED\"\n sr.error_message = \"Stale: concurrency deadlock — marked by scheduled trigger\"\n sr.completed_at = now\n db.commit()\n logger.explore(f\"Cleared {len(stale_runs)} stale PENDING run(s), proceeding with scheduled trigger\", {\n \"job_id\": job_id,\n \"stale_run_ids\": [sr.id for sr in stale_runs],\n })\n # Proceed to execute the scheduled run (do NOT return)\n else:\n logger.explore(\"Skipping scheduled run — concurrent run in progress\", {\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(UTC) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n logger.reason(\"Baseline expired — full translation\", {\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n\n # Determine trigger type based on execution_mode and baseline expiry\n if execution_mode == \"new_key_only\" and not baseline_expired:\n run.trigger_type = \"new_key_only\"\n elif execution_mode == \"new_key_only\" and baseline_expired:\n logger.reason(\"Baseline expired — falling back to full translation\", {\n \"job_id\": job_id, \"schedule_id\": schedule_id, \"age_days\": age.days,\n })\n run.trigger_type = \"scheduled\"\n else:\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n logger.explore(\"Scheduled translation execution failed\", {\n \"run_id\": run.id,\n \"error\": str(exec_err),\n })\n # Send notification on scheduled-run failure (FR-041, FR-048)\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation failed: job={job_id}\"\n body = (\n f\"Scheduled translation run failed for job '{job_id}'.\\n\"\n f\"Schedule: {schedule_id}\\n\"\n f\"Error: {exec_err}\\n\"\n f\"Time: {datetime.now(UTC).isoformat()}\"\n )\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception as notify_err:\n logger.warning(f\"Failed to send failure notification: {notify_err}\")\n\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(UTC)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(UTC)\n db.commit()\n\n logger.reflect(\"Scheduled translation complete\", {\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n logger.error(f\"[scheduled_translation] Unexpected error: {e}\")\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation CRASHED: job={job_id}\"\n body = f\"Scheduled translation for job '{job_id}' crashed unexpectedly.\\nError: {e}\\nTime: {datetime.now(UTC).isoformat()}\"\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception:\n pass\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n" + "body": "# #region execute_scheduled_translation [C:4] [TYPE Function]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and new-key-only fallback.\n# @PRE schedule_id is valid.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\n# @COMPLEXITY 4\n\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n execution_mode: str = \"full\",\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n seed_trace_id()\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n logger.info(f\"[scheduled_translation] Schedule inactive/missing: {schedule_id}\")\n return\n\n logger.reason(\"Scheduled translation triggered\", {\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n stale_threshold = timedelta(hours=1)\n now = datetime.now(UTC)\n\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .order_by(TranslationRun.created_at.asc()) # oldest first\n .first()\n )\n if active_run:\n is_stale = (\n active_run.status == \"PENDING\"\n and active_run.created_at\n and (now - active_run.created_at) > stale_threshold\n )\n if is_stale:\n # Mark ALL stale PENDING runs as FAILED\n stale_runs = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status == \"PENDING\",\n TranslationRun.created_at < (now - stale_threshold),\n )\n .all()\n )\n for sr in stale_runs:\n sr.status = \"FAILED\"\n sr.error_message = \"Stale: concurrency deadlock — marked by scheduled trigger\"\n sr.completed_at = now\n db.commit()\n logger.explore(f\"Cleared {len(stale_runs)} stale PENDING run(s), proceeding with scheduled trigger\", {\n \"job_id\": job_id,\n \"stale_run_ids\": [sr.id for sr in stale_runs],\n })\n # Proceed to execute the scheduled run (do NOT return)\n else:\n logger.explore(\"Skipping scheduled run — concurrent run in progress\", {\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(UTC) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n logger.reason(\"Baseline expired — full translation\", {\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n\n # Determine trigger type based on execution_mode and baseline expiry\n if execution_mode == \"new_key_only\" and not baseline_expired:\n run.trigger_type = \"new_key_only\"\n elif execution_mode == \"new_key_only\" and baseline_expired:\n logger.reason(\"Baseline expired — falling back to full translation\", {\n \"job_id\": job_id, \"schedule_id\": schedule_id, \"age_days\": age.days,\n })\n run.trigger_type = \"scheduled\"\n else:\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n if run.status == \"FAILED\":\n logger.explore(\"Scheduled translation completed with all records failed\", {\n \"run_id\": run.id,\n \"error\": run.error_message,\n })\n run.insert_status = run.insert_status or None\n else:\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n logger.explore(\"Scheduled translation execution failed\", {\n \"run_id\": run.id,\n \"error\": str(exec_err),\n })\n # Send notification on scheduled-run failure (FR-041, FR-048)\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation failed: job={job_id}\"\n body = (\n f\"Scheduled translation run failed for job '{job_id}'.\\n\"\n f\"Schedule: {schedule_id}\\n\"\n f\"Error: {exec_err}\\n\"\n f\"Time: {datetime.now(UTC).isoformat()}\"\n )\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception as notify_err:\n logger.warning(f\"Failed to send failure notification: {notify_err}\")\n\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(UTC)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(UTC)\n db.commit()\n\n logger.reflect(\"Scheduled translation complete\", {\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n logger.error(f\"[scheduled_translation] Unexpected error: {e}\")\n try:\n notify_service = NotificationService(db, config_manager)\n notify_service._initialize_providers()\n subject = f\"Scheduled translation CRASHED: job={job_id}\"\n body = f\"Scheduled translation for job '{job_id}' crashed unexpectedly.\\nError: {e}\\nTime: {datetime.now(UTC).isoformat()}\"\n import asyncio\n for provider in notify_service._providers.values():\n asyncio.run(provider.send(recipient=\"admin\", subject=subject, body=body))\n except Exception:\n pass\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n" }, { - "contract_id": "get_dialect_from_database", - "contract_type": "Function", + "contract_id": "TranslateJobService", + "contract_type": "Module", "file_path": "backend/src/plugins/translate/service.py", - "start_line": 41, - "end_line": 84, + "start_line": 1, + "end_line": 276, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, - "POST": "Returns normalized dialect string or raises ValueError if unsupported.", - "PRE": "database_record is a dict from Superset API.", - "PURPOSE": "Extract normalized dialect string from a Superset database record." + "LAYER": "Domain", + "POST": "Translation jobs are created/updated/deleted with column validation and dialect caching.", + "PRE": "Database session and config manager are available.", + "PURPOSE": "Service layer for translation job CRUD with datasource column validation and database dialect detection.", + "RATIONALE": "Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.", + "REJECTED": "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": [], + "relations": [ + { + "source_id": "TranslateJobService", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationJob", + "target_ref": "[TranslationJob]" + }, + { + "source_id": "TranslateJobService", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetClient", + "target_ref": "[SupersetClient]" + }, + { + "source_id": "TranslateJobService", + "relation_type": "DEPENDS_ON", + "target_id": "ConfigManager", + "target_ref": "[ConfigManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TranslateJobService [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, job, datasource, dialect]\n# @BRIEF Service layer for translation job CRUD with datasource column validation and database dialect detection.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Database session and config manager are available.\n# @POST Translation jobs are created/updated/deleted with column validation and dialect caching.\n# @SIDE_EFFECT Queries Superset for column metadata and database dialect at save time.\n# @RATIONALE Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.\n# @REJECTED Invalidating in-progress runs on config edit would break scheduled run continuity.\n# @REJECTED Monolithic service.py — violated INV_7. Decomposed into service_datasource, service_inline_correction, service_bulk_replace, service_utils.\n\nimport uuid\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, 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", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/service_bulk_replace.py", + "start_line": 1, + "end_line": 189, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Service for bulk find-and-replace operations on translated values." + }, + "relations": [ + { + "source_id": "BulkFindReplaceService", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationLanguage", + "target_ref": "[TranslationLanguage]" + }, + { + "source_id": "BulkFindReplaceService", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "BulkFindReplaceService", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryEntry", + "target_ref": "[DictionaryEntry]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region BulkFindReplaceService [C:3] [TYPE Class] [SEMANTICS translate, bulk, find, replace, regex]\n# @BRIEF Service for bulk find-and-replace operations on translated values.\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n\nimport re\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 .service_inline_correction import InlineCorrectionService\n\n\nclass BulkFindReplaceService:\n \"\"\"Handle bulk find-and-replace on TranslationLanguage entries.\"\"\"\n\n MAX_PATTERN_LENGTH = 500\n\n @staticmethod\n def _compile_pattern(pattern: str, is_regex: bool) -> re.Pattern:\n \"\"\"Compile a search pattern (regex or plain text).\"\"\"\n if len(pattern) > BulkFindReplaceService.MAX_PATTERN_LENGTH:\n raise ValueError(f\"Pattern too long (max {BulkFindReplaceService.MAX_PATTERN_LENGTH} characters)\")\n try:\n return re.compile(pattern) if is_regex else re.compile(re.escape(pattern))\n except re.error as e:\n raise ValueError(f\"Invalid regex pattern: {e}\")\n\n @staticmethod\n def _find_matching_entries(\n db: Session,\n run_id: str,\n pattern: str,\n is_regex: bool,\n target_language: str,\n ) -> list[Any]:\n \"\"\"Find all TranslationLanguage entries matching the pattern.\"\"\"\n entries = (\n db.query(TranslationLanguage)\n .join(TranslationRecord, TranslationRecord.id == TranslationLanguage.record_id)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationLanguage.language_code == target_language,\n )\n .all()\n )\n return entries\n\n @staticmethod\n def preview(\n db: Session,\n run_id: str,\n pattern: str,\n is_regex: bool,\n replacement_text: str,\n target_language: str,\n ) -> list[dict]:\n \"\"\"Scan TranslationLanguage entries and return matching items without applying changes.\"\"\"\n compiled = BulkFindReplaceService._compile_pattern(pattern, is_regex)\n entries = BulkFindReplaceService._find_matching_entries(\n db, run_id, pattern, is_regex, target_language\n )\n\n preview_items = []\n for entry in entries:\n current_value = entry.final_value or entry.translated_value or \"\"\n if compiled.search(current_value):\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == entry.record_id)\n .first()\n )\n source_term = record.source_sql if record else \"\"\n new_value = compiled.sub(replacement_text, current_value)\n if new_value != current_value:\n preview_items.append({\n \"record_id\": entry.record_id,\n \"language_code\": entry.language_code,\n \"source_term\": source_term or \"\",\n \"current_value\": current_value,\n \"new_value\": new_value,\n \"source_language_detected\": entry.source_language_detected,\n })\n return preview_items\n\n @staticmethod\n def apply(\n db: Session,\n run_id: str,\n pattern: str,\n is_regex: bool,\n replacement_text: str,\n target_language: str,\n submit_to_dictionary: bool = False,\n dictionary_id: str | None = None,\n usage_notes: str | None = None,\n current_user: str | None = None,\n submit_to_dictionary_with_context: bool = False,\n ) -> dict:\n \"\"\"Apply find-and-replace to matching TranslationLanguage entries.\"\"\"\n with belief_scope(\"BulkFindReplaceService.apply\"):\n compiled = BulkFindReplaceService._compile_pattern(pattern, is_regex)\n entries = BulkFindReplaceService._find_matching_entries(\n db, run_id, pattern, is_regex, target_language\n )\n\n preview_items = []\n rows_affected = 0\n corrections_submitted = 0\n seen_term_replacements: dict[str, str] = {}\n\n for entry in entries:\n current_value = entry.final_value or entry.translated_value or \"\"\n if compiled.search(current_value):\n new_value = compiled.sub(replacement_text, current_value)\n if new_value != current_value:\n entry.final_value = new_value\n entry.user_edit = new_value\n if entry.status in (\"pending\", \"translated\"):\n entry.status = \"edited\"\n rows_affected += 1\n\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == entry.record_id)\n .first()\n )\n source_term = record.source_sql if record else \"\"\n\n if submit_to_dictionary and dictionary_id:\n try:\n dict_result = InlineCorrectionService.submit_correction_to_dict(\n db=db, record_id=entry.record_id,\n language_code=entry.language_code,\n dictionary_id=dictionary_id,\n corrected_value=new_value,\n current_user=current_user,\n )\n if dict_result.get(\"action\") in (\"created\", \"updated\"):\n corrections_submitted += 1\n\n if submit_to_dictionary_with_context and record:\n term_key = f\"{source_term}::{new_value}\"\n if term_key not in seen_term_replacements:\n seen_term_replacements[term_key] = entry.record_id\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 if dict_result.get(\"action\") == \"created\" and dict_result.get(\"entry_id\"):\n d_entry = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.id == dict_result[\"entry_id\"])\n .first()\n )\n if d_entry:\n d_entry.context_data = context_data if context_data else None\n d_entry.has_context = bool(context_data)\n d_entry.context_source = \"bulk\"\n except Exception as e:\n logger.explore(\"Bulk replace: dictionary submission failed\", extra={\"record_id\": entry.record_id, \"error\": str(e)})\n\n preview_items.append({\n \"record_id\": entry.record_id,\n \"language_code\": entry.language_code,\n \"source_term\": source_term or \"\",\n \"current_value\": current_value,\n \"new_value\": new_value,\n \"source_language_detected\": entry.source_language_detected,\n })\n\n db.commit()\n\n result = {\n \"rows_affected\": rows_affected,\n \"corrections_submitted\": corrections_submitted,\n \"preview\": preview_items,\n }\n logger.reason(\"Bulk replace completed\", result)\n return result\n# #endregion BulkFindReplaceService\n" + }, + { + "contract_id": "DatasourceMetadataService", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/service_datasource.py", + "start_line": 1, + "end_line": 171, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "LAYER": "Domain", + "POST": "Datasource metadata is fetched with column details and normalized dialect.", + "PRE": "Database session and config manager are available.", + "PURPOSE": "Fetch datasource column metadata and database dialect from Superset." + }, + "relations": [ + { + "source_id": "DatasourceMetadataService", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetClient", + "target_ref": "[SupersetClient]" + }, + { + "source_id": "DatasourceMetadataService", + "relation_type": "DEPENDS_ON", + "target_id": "ConfigManager", + "target_ref": "[ConfigManager]" + } + ], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", "detail": { "actual_complexity": 4, - "contract_type": "Function" + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_dialect_from_database [C:4] [TYPE Function]\n# @BRIEF Extract normalized dialect string from a Superset database record.\n# @PRE: database_record is a dict from Superset API.\n# @POST: Returns normalized dialect string or raises ValueError if unsupported.\ndef get_dialect_from_database(database_record: dict[str, Any]) -> str:\n \"\"\"Extract and validate dialect from Superset database record.\"\"\"\n backend = (\n database_record.get(\"backend\")\n or database_record.get(\"engine\")\n or \"\"\n ).lower().strip()\n\n if not backend:\n raise ValueError(\"Could not determine database dialect from connection\")\n\n # Map Superset backend names to normalized dialect\n dialect_map = {\n \"postgresql\": \"postgresql\",\n \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\",\n \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\",\n \"sqlite\": \"sqlite\",\n \"mssql\": \"mssql\",\n \"oracle\": \"oracle\",\n \"snowflake\": \"snowflake\",\n \"bigquery\": \"bigquery\",\n \"redshift\": \"redshift\",\n \"presto\": \"presto\",\n \"trino\": \"trino\",\n \"druid\": \"druid\",\n \"hive\": \"hive\",\n \"spark\": \"spark\",\n \"databricks\": \"databricks\",\n }\n\n normalized = dialect_map.get(backend, backend)\n if normalized not in SUPPORTED_DIALECTS:\n raise ValueError(\n f\"Unsupported database dialect: '{backend}'. \"\n f\"Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}\"\n )\n return normalized\n# #endregion get_dialect_from_database\n" + "body": "# #region DatasourceMetadataService [C:4] [TYPE Module] [SEMANTICS superset, datasource, columns, dialect]\n# @BRIEF Fetch datasource column metadata and database dialect from Superset.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Database session and config manager are available.\n# @POST Datasource metadata is fetched with column details and normalized dialect.\n\nfrom typing import Any\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import logger\nfrom ...core.superset_client import SupersetClient\nfrom ...schemas.translate import DatasourceColumnResponse, DatasourceColumnsResponse\n\n# Supported database dialects for translation\nSUPPORTED_DIALECTS = {\n \"postgresql\", \"mysql\", \"clickhouse\", \"sqlite\", \"mssql\",\n \"oracle\", \"snowflake\", \"bigquery\", \"redshift\", \"presto\",\n \"trino\", \"druid\", \"hive\", \"spark\", \"databricks\",\n}\n\n\n# #region get_dialect_from_database [C:2] [TYPE Function]\n# @BRIEF Extract normalized dialect string from a Superset database record.\ndef get_dialect_from_database(database_record: dict[str, Any]) -> str:\n \"\"\"Extract and validate dialect from Superset database record.\"\"\"\n backend = (\n database_record.get(\"backend\")\n or database_record.get(\"engine\")\n or \"\"\n ).lower().strip()\n\n if not backend:\n raise ValueError(\"Could not determine database dialect from connection\")\n\n dialect_map = {\n \"postgresql\": \"postgresql\", \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\", \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\", \"sqlite\": \"sqlite\",\n \"mssql\": \"mssql\", \"oracle\": \"oracle\",\n \"snowflake\": \"snowflake\", \"bigquery\": \"bigquery\",\n \"redshift\": \"redshift\", \"presto\": \"presto\",\n \"trino\": \"trino\", \"druid\": \"druid\",\n \"hive\": \"hive\", \"spark\": \"spark\", \"databricks\": \"databricks\",\n }\n\n normalized = dialect_map.get(backend, backend)\n if normalized not in SUPPORTED_DIALECTS:\n raise ValueError(\n f\"Unsupported database dialect: '{backend}'. \"\n f\"Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}\"\n )\n return normalized\n# #endregion get_dialect_from_database\n\n\n# #region fetch_datasource_metadata [C:3] [TYPE Function]\n# @BRIEF Fetch datasource columns and database dialect from Superset.\ndef fetch_datasource_metadata(\n dataset_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> tuple[list[dict[str, Any]], str]:\n \"\"\"Fetch column metadata and database dialect for a datasource from Superset.\"\"\"\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found in configuration\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append({\n \"name\": str(col_name),\n \"type\": col.get(\"type\"),\n \"is_physical\": col.get(\"is_physical\", True),\n \"is_dttm\": col.get(\"is_dttm\", False),\n \"description\": col.get(\"description\", \"\"),\n })\n\n database_info = dataset_detail.get(\"database\", {})\n if isinstance(database_info, dict):\n dialect = get_dialect_from_database(database_info)\n else:\n try:\n db_id = dataset_detail.get(\"database_id\")\n if db_id:\n db_record = client.get_database(int(db_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"No database information available for this datasource\")\n except Exception as e:\n raise ValueError(f\"Could not determine database dialect: {e}\")\n\n return columns, dialect\n# #endregion fetch_datasource_metadata\n\n\n# #region detect_virtual_columns [C:2] [TYPE Function]\n# @BRIEF Identify virtual (calculated) columns from column metadata.\ndef detect_virtual_columns(columns: list[dict[str, Any]]) -> list[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n\n\n# #region get_datasource_columns [C:3] [TYPE Function]\n# @BRIEF Fetch datasource column metadata from Superset and return structured response.\ndef get_datasource_columns(\n datasource_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> DatasourceColumnsResponse:\n \"\"\"Fetch and return column metadata for a given Superset datasource.\"\"\"\n logger.info(f\"[get_datasource_columns] Fetching columns for datasource {datasource_id}\")\n\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(datasource_id)\n\n database_info = dataset_detail.get(\"database\", {})\n dialect = None\n if isinstance(database_info, dict):\n try:\n dialect = get_dialect_from_database(database_info)\n except (ValueError, KeyError):\n dialect = None\n if dialect is None:\n database_id = dataset_detail.get(\"database_id\")\n if database_id:\n db_record = client.get_database(int(database_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"Could not determine database dialect for this datasource\")\n\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append(DatasourceColumnResponse(\n name=str(col_name),\n type=col.get(\"type\"),\n is_physical=col.get(\"is_physical\", True),\n is_dttm=col.get(\"is_dttm\", False),\n description=col.get(\"description\", \"\"),\n ))\n\n return DatasourceColumnsResponse(\n datasource_id=datasource_id,\n datasource_name=dataset_detail.get(\"table_name\"),\n schema_name=dataset_detail.get(\"schema\"),\n database_dialect=dialect,\n columns=columns,\n )\n# #endregion get_datasource_columns\n# #endregion DatasourceMetadataService\n" + }, + { + "contract_id": "get_dialect_from_database", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/service_datasource.py", + "start_line": 24, + "end_line": 55, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Extract normalized dialect string from a Superset database record." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region get_dialect_from_database [C:2] [TYPE Function]\n# @BRIEF Extract normalized dialect string from a Superset database record.\ndef get_dialect_from_database(database_record: dict[str, Any]) -> str:\n \"\"\"Extract and validate dialect from Superset database record.\"\"\"\n backend = (\n database_record.get(\"backend\")\n or database_record.get(\"engine\")\n or \"\"\n ).lower().strip()\n\n if not backend:\n raise ValueError(\"Could not determine database dialect from connection\")\n\n dialect_map = {\n \"postgresql\": \"postgresql\", \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\", \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\", \"sqlite\": \"sqlite\",\n \"mssql\": \"mssql\", \"oracle\": \"oracle\",\n \"snowflake\": \"snowflake\", \"bigquery\": \"bigquery\",\n \"redshift\": \"redshift\", \"presto\": \"presto\",\n \"trino\": \"trino\", \"druid\": \"druid\",\n \"hive\": \"hive\", \"spark\": \"spark\", \"databricks\": \"databricks\",\n }\n\n normalized = dialect_map.get(backend, backend)\n if normalized not in SUPPORTED_DIALECTS:\n raise ValueError(\n f\"Unsupported database dialect: '{backend}'. \"\n f\"Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}\"\n )\n return normalized\n# #endregion get_dialect_from_database\n" }, { "contract_id": "fetch_datasource_metadata", "contract_type": "Function", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 87, - "end_line": 141, + "file_path": "backend/src/plugins/translate/service_datasource.py", + "start_line": 58, + "end_line": 104, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, - "POST": "Returns (columns_list, dialect_string) or raises on failure.", - "PRE": "dataset_id is a valid Superset dataset ID and environment has valid credentials.", + "COMPLEXITY": 3, "PURPOSE": "Fetch datasource columns and database dialect from Superset." }, "relations": [], @@ -49638,88 +53506,122 @@ { "code": "missing_required_tag", "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "message": "@RELATION is required for contract type 'Function' at C3", "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, + "actual_complexity": 3, "contract_type": "Function" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region fetch_datasource_metadata [C:4] [TYPE Function]\n# @BRIEF Fetch datasource columns and database dialect from Superset.\n# @PRE: dataset_id is a valid Superset dataset ID and environment has valid credentials.\n# @POST: Returns (columns_list, dialect_string) or raises on failure.\ndef fetch_datasource_metadata(\n dataset_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> tuple[list[dict[str, Any]], str]:\n \"\"\"Fetch column metadata and database dialect for a datasource from Superset.\"\"\"\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found in configuration\")\n\n # Create Superset client and fetch dataset detail\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append({\n \"name\": str(col_name),\n \"type\": col.get(\"type\"),\n \"is_physical\": col.get(\"is_physical\", True),\n \"is_dttm\": col.get(\"is_dttm\", False),\n \"description\": col.get(\"description\", \"\"),\n })\n\n # Extract database dialect from datasource\n database_info = dataset_detail.get(\"database\", {})\n if isinstance(database_info, dict):\n dialect = get_dialect_from_database(database_info)\n else:\n # Fallback: try to fetch database directly\n try:\n db_id = dataset_detail.get(\"database_id\")\n if db_id:\n db_record = client.get_database(int(db_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"No database information available for this datasource\")\n except Exception as e:\n logger.warning(f\"[translate_service] Could not fetch database dialect: {e}\")\n raise ValueError(f\"Could not determine database dialect: {e}\")\n\n return columns, dialect\n# #endregion fetch_datasource_metadata\n" + "body": "# #region fetch_datasource_metadata [C:3] [TYPE Function]\n# @BRIEF Fetch datasource columns and database dialect from Superset.\ndef fetch_datasource_metadata(\n dataset_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> tuple[list[dict[str, Any]], str]:\n \"\"\"Fetch column metadata and database dialect for a datasource from Superset.\"\"\"\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found in configuration\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append({\n \"name\": str(col_name),\n \"type\": col.get(\"type\"),\n \"is_physical\": col.get(\"is_physical\", True),\n \"is_dttm\": col.get(\"is_dttm\", False),\n \"description\": col.get(\"description\", \"\"),\n })\n\n database_info = dataset_detail.get(\"database\", {})\n if isinstance(database_info, dict):\n dialect = get_dialect_from_database(database_info)\n else:\n try:\n db_id = dataset_detail.get(\"database_id\")\n if db_id:\n db_record = client.get_database(int(db_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"No database information available for this datasource\")\n except Exception as e:\n raise ValueError(f\"Could not determine database dialect: {e}\")\n\n return columns, dialect\n# #endregion fetch_datasource_metadata\n" }, { "contract_id": "detect_virtual_columns", "contract_type": "Function", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 144, - "end_line": 151, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns list of virtual column names.", - "PRE": "columns is a list of dicts with 'is_physical' key.", - "PURPOSE": "Identify virtual (calculated) columns from column metadata." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region detect_virtual_columns [C:4] [TYPE Function]\n# @BRIEF Identify virtual (calculated) columns from column metadata.\n# @PRE: columns is a list of dicts with 'is_physical' key.\n# @POST: Returns list of virtual column names.\ndef detect_virtual_columns(columns: list[dict[str, Any]]) -> list[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n" - }, - { - "contract_id": "TranslateJobService", - "contract_type": "Class", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 154, - "end_line": 468, + "file_path": "backend/src/plugins/translate/service_datasource.py", + "start_line": 107, + "end_line": 112, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "PURPOSE": "Service for translation job CRUD with validation and Superset integration." + "COMPLEXITY": 2, + "PURPOSE": "Identify virtual (calculated) columns from column metadata." }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslateJobService [TYPE Class]\n# @BRIEF Service for translation job CRUD with validation and Superset integration.\nclass TranslateJobService:\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 # @POST: Returns tuple of (total_count, list_of_jobs).\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\n if status_filter:\n query = query.filter(TranslationJob.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n jobs = query.order_by(TranslationJob.created_at.desc()).offset(offset).limit(page_size).all()\n\n return total, jobs\n # endregion list_jobs\n\n # region get_job [TYPE Function]\n # @PURPOSE: Get a single translation job by ID.\n # @POST: Returns TranslationJob or raises ValueError.\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 # @PRE: payload contains valid job configuration.\n # @POST: Returns the created TranslationJob with database_dialect cached.\n # @SIDE_EFFECT: Validates columns via SupersetClient if source_datasource_id is provided.\n # @SIDE_EFFECT: Caches database_dialect from Superset connection.\n def create_job(self, payload: TranslateJobCreate) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Creating job '{payload.name}'\")\n\n # Validate: must have a translation column if datasource is configured\n if payload.source_datasource_id and not payload.translation_column:\n raise ValueError(\"A translation column is required when a datasource is selected\")\n\n # Validate upsert strategy\n valid_strategies = {\"MERGE\", \"INSERT\", \"UPDATE\"}\n if payload.upsert_strategy not in valid_strategies:\n raise ValueError(\n f\"Invalid upsert_strategy '{payload.upsert_strategy}'. \"\n f\"Must be one of: {', '.join(sorted(valid_strategies))}\"\n )\n\n # Detect database dialect and validate columns if datasource is specified\n dialect = payload.database_dialect\n if payload.source_datasource_id and (payload.environment_id or payload.source_dialect):\n # If no explicit dialect, try to detect it\n if not dialect:\n try:\n env_id = payload.environment_id or payload.source_dialect\n _, detected_dialect = fetch_datasource_metadata(\n int(payload.source_datasource_id),\n env_id,\n self.config_manager,\n )\n dialect = detected_dialect\n except Exception as e:\n logger.warning(f\"[TranslateJobService] Dialect detection failed: {e}\")\n dialect = payload.source_dialect\n\n # Resolve target_languages: accept new format (list) or legacy single language\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 # Build job instance\n job = TranslationJob(\n id=str(uuid.uuid4()),\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n database_dialect=dialect,\n source_datasource_id=payload.source_datasource_id,\n source_table=payload.source_table,\n target_schema=payload.target_schema,\n target_table=payload.target_table,\n source_key_cols=payload.source_key_cols or [],\n target_key_cols=payload.target_key_cols or [],\n translation_column=payload.translation_column,\n 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 [],\n target_languages=target_languages,\n provider_id=payload.provider_id,\n batch_size=payload.batch_size,\n upsert_strategy=payload.upsert_strategy,\n environment_id=payload.environment_id,\n target_database_id=payload.target_database_id,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(job)\n self.db.flush()\n\n # Attach dictionaries\n if payload.dictionary_ids:\n for dict_id in payload.dictionary_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job.id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n 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 # @PRE: payload contains fields to update.\n # @POST: Returns the updated TranslationJob.\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\n update_data = payload.model_dump(exclude_unset=True)\n dict_ids = update_data.pop(\"dictionary_ids\", None)\n\n # Backward compat: if only target_language is set (old API), wrap into target_languages\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 elif \"target_languages\" in update_data:\n # target_languages is the canonical field now\n pass\n\n # Validate BCP-47 for target_languages\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 # Re-detect dialect if datasource changed\n if payload.source_datasource_id and not payload.database_dialect:\n try:\n env_id = (payload.environment_id or payload.source_dialect or job.environment_id or job.source_dialect)\n _, detected_dialect = fetch_datasource_metadata(\n int(payload.source_datasource_id),\n env_id,\n self.config_manager,\n )\n job.database_dialect = detected_dialect\n except Exception as e:\n logger.warning(f\"[TranslateJobService] Dialect re-detection failed: {e}\")\n\n job.updated_at = datetime.now(UTC)\n\n # Update dictionary associations if provided\n if dict_ids is not None:\n self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).delete()\n for dict_id in dict_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job_id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n 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 # @PRE: job_id must exist.\n # @POST: Job and all related records are deleted.\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\n # Delete dictionary associations\n self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).delete()\n\n self.db.delete(job)\n self.db.commit()\n 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 # @PRE: job_id must exist.\n # @POST: Returns the duplicated TranslationJob with status DRAFT.\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\n # Copy all fields except id, created_at, updated_at, status\n new_job = TranslationJob(\n id=str(uuid.uuid4()),\n name=new_name or f\"{source.name} (Copy)\",\n description=source.description,\n source_dialect=source.source_dialect,\n target_dialect=source.target_dialect,\n database_dialect=source.database_dialect,\n source_datasource_id=source.source_datasource_id,\n source_table=source.source_table,\n target_schema=source.target_schema,\n target_table=source.target_table,\n source_key_cols=source.source_key_cols,\n target_key_cols=source.target_key_cols,\n translation_column=source.translation_column,\n 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,\n target_languages=source.target_languages,\n provider_id=source.provider_id,\n batch_size=source.batch_size,\n upsert_strategy=source.upsert_strategy,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(new_job)\n self.db.flush()\n\n # Copy dictionary associations\n old_dicts = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n for assoc in old_dicts:\n new_assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=new_job.id,\n dictionary_id=assoc.dictionary_id,\n )\n self.db.add(new_assoc)\n\n self.db.commit()\n self.db.refresh(new_job)\n 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 # @POST: Returns list of dictionary IDs.\n def get_job_dictionary_ids(self, job_id: str) -> list[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n return [a.dictionary_id for a in assocs]\n # endregion get_job_dictionary_ids\n\n # region fetch_available_datasources [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 \"\"\"List Superset datasets available for translation.\"\"\"\n from ...core.superset_client import SupersetClient\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment '{env_id}' not found\")\n client = SupersetClient(env)\n _, datasets = client.get_datasets()\n result = []\n for ds in datasets:\n name = ds.get(\"table_name\", \"\")\n if search and search.lower() not in name.lower():\n continue\n db_info = ds.get(\"database\", {})\n backend = db_info.get(\"backend\", \"\")\n dialect = _extract_dialect(backend)\n result.append({\n \"id\": ds.get(\"id\"),\n \"table_name\": name,\n \"schema\": ds.get(\"schema\"),\n \"database_name\": db_info.get(\"database_name\", \"Unknown\"),\n \"database_dialect\": dialect,\n \"description\": ds.get(\"description\", \"\"),\n })\n return result\n # endregion fetch_available_datasources\n# #endregion TranslateJobService\n" + "body": "# #region detect_virtual_columns [C:2] [TYPE Function]\n# @BRIEF Identify virtual (calculated) columns from column metadata.\ndef detect_virtual_columns(columns: list[dict[str, Any]]) -> list[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n" + }, + { + "contract_id": "InlineCorrectionService", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/service_inline_correction.py", + "start_line": 1, + "end_line": 222, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Inline edits applied with optional dictionary submission.", + "PRE": "Database session is available.", + "PURPOSE": "Service for inline editing translated values and submitting corrections to dictionaries.", + "SIDE_EFFECT": "Modifies TranslationLanguage entries; optionally creates DictionaryEntry rows." + }, + "relations": [ + { + "source_id": "InlineCorrectionService", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationLanguage", + "target_ref": "[TranslationLanguage]" + }, + { + "source_id": "InlineCorrectionService", + "relation_type": "DEPENDS_ON", + "target_id": "TranslationRecord", + "target_ref": "[TranslationRecord]" + }, + { + "source_id": "InlineCorrectionService", + "relation_type": "DEPENDS_ON", + "target_id": "DictionaryEntry", + "target_ref": "[DictionaryEntry]" + }, + { + "source_id": "InlineCorrectionService", + "relation_type": "DEPENDS_ON", + "target_id": "_normalize_term", + "target_ref": "[_normalize_term]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region InlineCorrectionService [C:4] [TYPE Class] [SEMANTICS translate, correction, inline, dictionary]\n# @BRIEF Service for inline editing translated values and submitting corrections to dictionaries.\n# @PRE Database session is available.\n# @POST Inline edits applied with optional dictionary submission.\n# @SIDE_EFFECT Modifies TranslationLanguage entries; optionally creates DictionaryEntry rows.\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [_normalize_term]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TranslationLanguage, TranslationRecord\nfrom ._utils import _normalize_term\n\n\nclass InlineCorrectionService:\n \"\"\"Handle inline correction of translated values with optional dictionary submission.\"\"\"\n\n @staticmethod\n def apply_inline_edit(\n db: Session,\n run_id: str,\n record_id: str,\n language_code: str,\n final_value: str,\n submit_to_dictionary: bool = False,\n dictionary_id: str | None = None,\n current_user: str | None = None,\n context_data_override: dict | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict:\n \"\"\"Apply an inline edit to a TranslationLanguage entry.\n\n Updates final_value and user_edit fields. Optionally submits to dictionary.\n Returns the updated TranslationLanguage data as a dict.\n\n Args:\n context_data_override: If provided, overrides auto-captured context data.\n usage_notes: Usage notes for the dictionary entry.\n keep_context: If False, clear context on the dictionary entry.\n \"\"\"\n lang_entry = (\n db.query(TranslationLanguage)\n .filter(\n TranslationLanguage.record_id == record_id,\n TranslationLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(f\"Language entry '{language_code}' not found for record '{record_id}'\")\n\n record = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.id == record_id,\n TranslationRecord.run_id == run_id,\n )\n .first()\n )\n if not record:\n raise ValueError(f\"Translation record '{record_id}' not found in run '{run_id}'\")\n\n lang_entry.final_value = final_value\n lang_entry.user_edit = final_value\n if lang_entry.status in (\"pending\", \"translated\"):\n lang_entry.status = \"edited\"\n db.flush()\n\n dict_result = None\n if submit_to_dictionary and dictionary_id:\n try:\n dict_result = InlineCorrectionService.submit_correction_to_dict(\n db=db, record_id=record_id, language_code=language_code,\n dictionary_id=dictionary_id, corrected_value=final_value,\n current_user=current_user, context_data_override=context_data_override,\n usage_notes=usage_notes, keep_context=keep_context,\n )\n except Exception as e:\n dict_result = {\"error\": str(e), \"action\": \"failed\"}\n\n db.commit()\n db.refresh(lang_entry)\n\n from ...schemas.translate import TranslationLanguageResponse\n response = TranslationLanguageResponse.model_validate(lang_entry)\n result = response.model_dump()\n if dict_result:\n result[\"dictionary_result\"] = dict_result\n return result\n\n @staticmethod\n def submit_correction_to_dict(\n db: Session,\n record_id: str,\n language_code: str,\n dictionary_id: str,\n corrected_value: str,\n current_user: str | None = None,\n context_data_override: dict | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict:\n \"\"\"Submit a correction from an inline edit to the dictionary.\n\n Fetches the TranslationLanguage entry, auto-captures source text and context,\n and creates/updates a DictionaryEntry.\n\n Args:\n context_data_override: If provided, overrides auto-captured context_data.\n usage_notes: Optional usage notes to store on the dictionary entry.\n keep_context: If False, sets has_context=False and clears context_data.\n \"\"\"\n with belief_scope(\"InlineCorrectionService.submit_correction_to_dict\"):\n lang_entry = (\n db.query(TranslationLanguage)\n .filter(\n TranslationLanguage.record_id == record_id,\n TranslationLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(f\"Language entry '{language_code}' not found for record '{record_id}'\")\n\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == record_id)\n .first()\n )\n if not record:\n raise ValueError(f\"Translation record '{record_id}' not found\")\n\n source_term = record.source_sql or record.source_object_name or \"\"\n if not source_term:\n raise ValueError(\"No source text available for this record\")\n\n source_language = lang_entry.source_language_detected or \"und\"\n target_language = language_code\n\n context_data = {}\n if record.source_data:\n context_data[\"source_data\"] = record.source_data\n if record.source_object_type:\n context_data[\"source_object_type\"] = record.source_object_type\n if record.source_object_name:\n context_data[\"source_object_name\"] = record.source_object_name\n if record.source_object_id:\n context_data[\"source_object_id\"] = record.source_object_id\n\n if context_data_override is not None:\n context_data = context_data_override\n context_source = \"manual\"\n else:\n context_source = \"auto\"\n\n if not keep_context:\n context_data = None\n context_source = \"manual\"\n\n normalized = _normalize_term(source_term)\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n\n result = {\"action\": \"created\", \"entry_id\": None, \"conflict\": None, \"message\": None}\n\n if existing:\n result[\"action\"] = \"conflict_detected\"\n result[\"conflict\"] = {\n \"source_term\": source_term,\n \"existing_target_term\": existing.target_term,\n \"submitted_target_term\": corrected_value,\n \"action\": \"keep_existing\",\n }\n result[\"message\"] = (\n f\"Existing entry found: '{existing.target_term}' \"\n f\"for source '{source_term}' ({source_language} → {target_language})\"\n )\n logger.reason(\"Correction conflict detected\", result)\n return result\n\n entry = DictionaryEntry(\n dictionary_id=dictionary_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=corrected_value.strip(),\n source_language=source_language,\n target_language=target_language,\n context_data=context_data if context_data else None,\n context_notes=None,\n has_context=bool(context_data) and keep_context,\n context_source=context_source,\n usage_notes=usage_notes,\n origin_source_language=source_language,\n origin_run_id=record.run_id,\n origin_row_key=record.id,\n origin_user_id=current_user,\n )\n db.add(entry)\n db.flush()\n result[\"entry_id\"] = entry.id\n result[\"message\"] = (\n f\"Entry created for '{source_term}' ({source_language}) → \"\n f\"'{corrected_value}' ({target_language})\"\n )\n logger.reflect(\"Dictionary entry created from correction\", result)\n return result\n# #endregion InlineCorrectionService\n" + }, + { + "contract_id": "ServiceUtils", + "contract_type": "Module", + "file_path": "backend/src/plugins/translate/service_utils.py", + "start_line": 1, + "end_line": 60, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Utility functions for the translate service layer." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region ServiceUtils [C:1] [TYPE Module] [SEMANTICS utils, dialect, response]\n# @BRIEF Utility functions for the translate service layer.\n\nfrom typing import Any\n\nfrom ...models.translate import TranslationJob\nfrom ...schemas.translate import TranslateJobResponse\n\n\n# #region _extract_dialect [TYPE Function]\n# @BRIEF Extract database dialect from Superset backend URI.\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI.\"\"\"\n if not backend:\n return \"unknown\"\n try:\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n return dialect.lower()\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n\n\n# #region job_to_response [TYPE Function]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: list[str] | None = None) -> TranslateJobResponse:\n return TranslateJobResponse(\n id=job.id,\n name=job.name,\n description=job.description,\n source_dialect=job.source_dialect,\n target_dialect=job.target_dialect,\n database_dialect=job.database_dialect,\n source_datasource_id=job.source_datasource_id,\n source_table=job.source_table,\n target_schema=job.target_schema,\n target_table=job.target_table,\n source_key_cols=job.source_key_cols or [],\n target_key_cols=job.target_key_cols or [],\n translation_column=job.translation_column,\n target_column=job.target_column,\n target_language_column=job.target_language_column,\n target_source_column=job.target_source_column,\n target_source_language_column=job.target_source_language_column,\n context_columns=job.context_columns or [],\n target_languages=job.target_languages,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n# #endregion ServiceUtils\n" + }, + { + "contract_id": "_extract_dialect", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/service_utils.py", + "start_line": 10, + "end_line": 22, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "PURPOSE": "Extract database dialect from Superset backend URI." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _extract_dialect [TYPE Function]\n# @BRIEF Extract database dialect from Superset backend URI.\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI.\"\"\"\n if not backend:\n return \"unknown\"\n try:\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n return dialect.lower()\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n" }, { "contract_id": "job_to_response", "contract_type": "Function", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 485, - "end_line": 519, + "file_path": "backend/src/plugins/translate/service_utils.py", + "start_line": 25, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49731,93 +53633,6 @@ "tier_source": "AutoCalculated", "body": "# #region job_to_response [TYPE Function]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: list[str] | None = None) -> TranslateJobResponse:\n return TranslateJobResponse(\n id=job.id,\n name=job.name,\n description=job.description,\n source_dialect=job.source_dialect,\n target_dialect=job.target_dialect,\n database_dialect=job.database_dialect,\n source_datasource_id=job.source_datasource_id,\n source_table=job.source_table,\n target_schema=job.target_schema,\n target_table=job.target_table,\n source_key_cols=job.source_key_cols or [],\n target_key_cols=job.target_key_cols or [],\n translation_column=job.translation_column,\n target_column=job.target_column,\n target_language_column=job.target_language_column,\n target_source_column=job.target_source_column,\n target_source_language_column=job.target_source_language_column,\n context_columns=job.context_columns or [],\n target_languages=job.target_languages,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n" }, - { - "contract_id": "DatasourceColumnsService", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 522, - "end_line": 586, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "POST": "Returns DatasourceColumnsResponse with column metadata and database dialect.", - "PRE": "datasource_id is a valid Superset dataset ID.", - "PURPOSE": "Fetch datasource column metadata from Superset and return structured response.", - "SIDE_EFFECT": "Queries Superset API for dataset detail and database info." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region DatasourceColumnsService [C:4] [TYPE Function]\n# @BRIEF Fetch datasource column metadata from Superset and return structured response.\n# @PRE datasource_id is a valid Superset dataset ID.\n# @POST Returns DatasourceColumnsResponse with column metadata and database dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @COMPLEXITY 4\n\ndef get_datasource_columns(\n datasource_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> DatasourceColumnsResponse:\n \"\"\"Fetch and return column metadata for a given Superset datasource.\"\"\"\n logger.info(f\"[get_datasource_columns] Fetching columns for datasource {datasource_id}\")\n\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n # Create Superset client\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(datasource_id)\n\n # Extract database dialect\n database_info = dataset_detail.get(\"database\", {})\n dialect = None\n if isinstance(database_info, dict):\n try:\n dialect = get_dialect_from_database(database_info)\n except (ValueError, KeyError):\n dialect = None\n if dialect is None:\n database_id = dataset_detail.get(\"database_id\")\n if database_id:\n db_record = client.get_database(int(database_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"Could not determine database dialect for this datasource\")\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append(DatasourceColumnResponse(\n name=str(col_name),\n type=col.get(\"type\"),\n is_physical=col.get(\"is_physical\", True),\n is_dttm=col.get(\"is_dttm\", False),\n description=col.get(\"description\", \"\"),\n ))\n\n return DatasourceColumnsResponse(\n datasource_id=datasource_id,\n datasource_name=dataset_detail.get(\"table_name\"),\n schema_name=dataset_detail.get(\"schema\"),\n database_dialect=dialect,\n columns=columns,\n )\n# #endregion DatasourceColumnsService\n" - }, - { - "contract_id": "InlineCorrectionService", - "contract_type": "Class", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 589, - "end_line": 828, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Service for inline editing translated values and submitting corrections to dictionaries." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region InlineCorrectionService [C:3] [TYPE Class] [SEMANTICS translate, correction, inline, dictionary]\n# @BRIEF Service for inline editing translated values and submitting corrections to dictionaries.\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 from ...models.translate import TranslationLanguage, TranslationRecord\n\n # Find the language entry\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 # Verify the record belongs to the run\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(\n f\"Translation record '{record_id}' not found in run '{run_id}'\"\n )\n\n # Apply the edit\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 # Optionally submit to dictionary\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,\n record_id=record_id,\n language_code=language_code,\n dictionary_id=dictionary_id,\n corrected_value=final_value,\n current_user=current_user,\n context_data_override=context_data_override,\n usage_notes=usage_notes,\n keep_context=keep_context,\n )\n except Exception as e:\n # Dictionary submission failure should not block the edit\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 with correct language pair.\n Returns conflict info if an entry already exists.\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 from ...core.logger import belief_scope, logger\n from ...models.translate import DictionaryEntry, TranslationLanguage, TranslationRecord\n from ._utils import _normalize_term\n\n with belief_scope(\"InlineCorrectionService.submit_correction_to_dict\"):\n # Find the language entry\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 # Find the record for source text and context\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 # Get source text from the record\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 # Determine language pair\n source_language = lang_entry.source_language_detected or \"und\"\n target_language = language_code\n\n # Prepare context data — auto-capture from source row\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 # Apply context_data_override if provided (user edited)\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 # Handle keep_context=False (user explicitly removed context)\n if not keep_context:\n context_data = None\n context_source = \"manual\"\n\n # Normalize source term\n normalized = _normalize_term(source_term)\n\n # Check for existing entry with same (dictionary_id, source_term_norm, source_language, target_language)\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 = {\n \"action\": \"created\",\n \"entry_id\": None,\n \"conflict\": None,\n \"message\": None,\n }\n\n if existing:\n # Conflict: return conflict info\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 # Create new entry\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": "BulkFindReplaceService", - "contract_type": "Class", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 831, - "end_line": 1048, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Service for bulk find-and-replace operations on translated values." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region BulkFindReplaceService [C:3] [TYPE Class] [SEMANTICS translate, bulk, find, replace, regex]\n# @BRIEF Service for bulk find-and-replace operations on translated values.\nclass BulkFindReplaceService:\n \"\"\"Handle bulk find-and-replace on TranslationLanguage entries.\"\"\"\n\n MAX_PATTERN_LENGTH = 500\n\n @staticmethod\n def _compile_pattern(pattern: str, is_regex: bool) -> re.Pattern:\n \"\"\"Compile a search pattern (regex or plain text).\"\"\"\n if len(pattern) > BulkFindReplaceService.MAX_PATTERN_LENGTH:\n raise ValueError(\n f\"Pattern too long (max {BulkFindReplaceService.MAX_PATTERN_LENGTH} characters)\"\n )\n try:\n return re.compile(pattern) if is_regex else re.compile(re.escape(pattern))\n except re.error as e:\n raise ValueError(f\"Invalid regex pattern: {e}\")\n\n @staticmethod\n def _find_matching_entries(\n db: Session,\n run_id: str,\n pattern: str,\n is_regex: bool,\n target_language: str,\n ) -> list[Any]:\n \"\"\"Find all TranslationLanguage entries matching the pattern.\"\"\"\n from ...models.translate import TranslationLanguage, TranslationRecord\n\n # We scan entries for the target language in this run\n entries = (\n db.query(TranslationLanguage)\n .join(\n TranslationRecord,\n TranslationRecord.id == TranslationLanguage.record_id,\n )\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationLanguage.language_code == target_language,\n )\n .all()\n )\n return entries\n\n @staticmethod\n def preview(\n db: Session,\n run_id: str,\n pattern: str,\n is_regex: bool,\n replacement_text: str,\n target_language: str,\n ) -> list[dict]:\n \"\"\"Scan TranslationLanguage entries and return matching items without applying changes.\"\"\"\n from ...models.translate import TranslationRecord\n\n compiled = BulkFindReplaceService._compile_pattern(pattern, is_regex)\n entries = BulkFindReplaceService._find_matching_entries(\n db, run_id, pattern, is_regex, target_language\n )\n\n preview_items = []\n for entry in entries:\n current_value = entry.final_value or entry.translated_value or \"\"\n if compiled.search(current_value):\n # Get source term for context\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == entry.record_id)\n .first()\n )\n source_term = record.source_sql if record else \"\"\n\n new_value = compiled.sub(\n replacement_text,\n current_value,\n )\n if new_value != current_value:\n preview_items.append({\n \"record_id\": entry.record_id,\n \"language_code\": entry.language_code,\n \"source_term\": source_term or \"\",\n \"current_value\": current_value,\n \"new_value\": new_value,\n \"source_language_detected\": entry.source_language_detected,\n })\n\n return preview_items\n\n @staticmethod\n def _get_replacement(replacement_text: str, is_regex: bool) -> str:\n \"\"\"Return replacement pattern (for regex, use backrefs; for plain, use literal).\"\"\"\n return replacement_text\n\n @staticmethod\n def apply(\n db: Session,\n run_id: str,\n pattern: str,\n is_regex: bool,\n replacement_text: str,\n target_language: str,\n submit_to_dictionary: bool = False,\n dictionary_id: str | None = None,\n usage_notes: str | None = None,\n current_user: str | None = None,\n submit_to_dictionary_with_context: bool = False,\n ) -> dict:\n \"\"\"Apply find-and-replace to matching TranslationLanguage entries.\n\n Returns dict with rows_affected, corrections_submitted, and preview list.\n\n Args:\n submit_to_dictionary_with_context: If True, auto-capture source row context\n when submitting to dictionary.\n \"\"\"\n from ...core.logger import belief_scope, logger\n from ...models.translate import DictionaryEntry, TranslationRecord\n\n with belief_scope(\"BulkFindReplaceService.apply\"):\n compiled = BulkFindReplaceService._compile_pattern(pattern, is_regex)\n entries = BulkFindReplaceService._find_matching_entries(\n db, run_id, pattern, is_regex, target_language\n )\n\n preview_items = []\n rows_affected = 0\n corrections_submitted = 0\n\n # Track unique (source_term, replacement) -> first record for context capture\n seen_term_replacements: dict[str, str] = {}\n\n for entry in entries:\n current_value = entry.final_value or entry.translated_value or \"\"\n if compiled.search(current_value):\n new_value = compiled.sub(replacement_text, current_value)\n if new_value != current_value:\n # Apply the replacement\n entry.final_value = new_value\n entry.user_edit = new_value\n if entry.status in (\"pending\", \"translated\"):\n entry.status = \"edited\"\n rows_affected += 1\n\n # Get source term for preview\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == entry.record_id)\n .first()\n )\n source_term = record.source_sql if record else \"\"\n\n # Optionally submit to dictionary\n if submit_to_dictionary and dictionary_id:\n try:\n dict_result = InlineCorrectionService.submit_correction_to_dict(\n db=db,\n record_id=entry.record_id,\n language_code=entry.language_code,\n dictionary_id=dictionary_id,\n corrected_value=new_value,\n current_user=current_user,\n )\n if dict_result.get(\"action\") in (\"created\", \"updated\"):\n corrections_submitted += 1\n\n # Track context for unique (source_term, replacement) pairs\n if submit_to_dictionary_with_context and record:\n term_key = f\"{source_term}::{new_value}\"\n if term_key not in seen_term_replacements:\n seen_term_replacements[term_key] = entry.record_id\n # Auto-capture context from this row\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 # Update the just-created entry's context\n if dict_result.get(\"action\") == \"created\" and dict_result.get(\"entry_id\"):\n d_entry = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.id == dict_result[\"entry_id\"])\n .first()\n )\n if d_entry:\n d_entry.context_data = context_data if context_data else None\n d_entry.has_context = bool(context_data)\n d_entry.context_source = \"bulk\"\n except Exception as e:\n logger.explore(\n \"Bulk replace: dictionary submission failed\",\n extra={\"record_id\": entry.record_id, \"error\": str(e)},\n )\n\n preview_items.append({\n \"record_id\": entry.record_id,\n \"language_code\": entry.language_code,\n \"source_term\": source_term or \"\",\n \"current_value\": current_value,\n \"new_value\": new_value,\n \"source_language_detected\": entry.source_language_detected,\n })\n\n db.commit()\n\n result = {\n \"rows_affected\": rows_affected,\n \"corrections_submitted\": corrections_submitted,\n \"preview\": preview_items,\n }\n logger.reason(\"Bulk replace completed\", result)\n return result\n# #endregion BulkFindReplaceService\n" - }, { "contract_id": "SQLGenerator", "contract_type": "Module", @@ -50235,11 +54050,12 @@ "contract_type": "Module", "file_path": "backend/src/schemas/auth.py", "start_line": 1, - "end_line": 157, - "tier": "TIER_2", - "complexity": 3, + "end_line": 158, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "AuthPayload -> AuthSchema", "INVARIANT": "Sensitive fields like password must not be included in response schemas.", "LAYER": "API", "PURPOSE": "Pydantic schemas for authentication requests and responses." @@ -50254,25 +54070,43 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, auth, schema, token]\n#\n# @BRIEF Pydantic schemas for authentication requests and responses.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> pydantic\n#\n# @INVARIANT: Sensitive fields like password must not be included in response schemas.\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, EmailStr\n\n\n# #region Token [C:1] [TYPE Class]\n# @BRIEF Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# #endregion Token\n\n\n# #region TokenData [C:1] [TYPE Class]\n# @BRIEF Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: str | None = None\n scopes: list[str] = []\n\n\n# #endregion TokenData\n\n\n# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion PermissionSchema\n\n\n# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion RoleSchema\n\n\n# #region RoleCreate [TYPE Class]\n# @BRIEF Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: str | None = None\n permissions: list[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# #endregion RoleCreate\n\n\n# #region RoleUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n permissions: list[str] | None = None\n\n\n# #endregion RoleUpdate\n\n\n# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF Represents an AD Group to Role mapping in API responses.\nclass ADGroupMappingSchema(BaseModel):\n id: str\n ad_group: str\n role_id: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion ADGroupMappingSchema\n\n\n# #region ADGroupMappingCreate [TYPE Class]\n# @BRIEF Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n\n# #endregion ADGroupMappingCreate\n\n\n# #region UserBase [TYPE Class]\n# @BRIEF Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: EmailStr | None = None\n is_active: bool = True\n\n\n# #endregion UserBase\n\n\n# #region UserCreate [TYPE Class]\n# @BRIEF Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str\n roles: list[str] = []\n\n\n# #endregion UserCreate\n\n\n# #region UserUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: EmailStr | None = None\n password: str | None = None\n is_active: bool | None = None\n roles: list[str] | None = None\n\n\n# #endregion UserUpdate\n\n\n# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion User\n\n# #endregion AuthSchemas\n" + "body": "# #region AuthSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, auth, schema, token]\n#\n# @BRIEF Pydantic schemas for authentication requests and responses.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> pydantic\n#\n# @INVARIANT: Sensitive fields like password must not be included in response schemas.\n# @DATA_CONTRACT: AuthPayload -> AuthSchema\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, EmailStr\n\n\n# #region Token [C:1] [TYPE Class]\n# @BRIEF Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# #endregion Token\n\n\n# #region TokenData [C:1] [TYPE Class]\n# @BRIEF Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: str | None = None\n scopes: list[str] = []\n\n\n# #endregion TokenData\n\n\n# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion PermissionSchema\n\n\n# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion RoleSchema\n\n\n# #region RoleCreate [TYPE Class]\n# @BRIEF Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: str | None = None\n permissions: list[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# #endregion RoleCreate\n\n\n# #region RoleUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n permissions: list[str] | None = None\n\n\n# #endregion RoleUpdate\n\n\n# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF Represents an AD Group to Role mapping in API responses.\nclass ADGroupMappingSchema(BaseModel):\n id: str\n ad_group: str\n role_id: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion ADGroupMappingSchema\n\n\n# #region ADGroupMappingCreate [TYPE Class]\n# @BRIEF Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n\n# #endregion ADGroupMappingCreate\n\n\n# #region UserBase [TYPE Class]\n# @BRIEF Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: EmailStr | None = None\n is_active: bool = True\n\n\n# #endregion UserBase\n\n\n# #region UserCreate [TYPE Class]\n# @BRIEF Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str\n roles: list[str] = []\n\n\n# #endregion UserCreate\n\n\n# #region UserUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: EmailStr | None = None\n password: str | None = None\n is_active: bool | None = None\n roles: list[str] | None = None\n\n\n# #endregion UserUpdate\n\n\n# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion User\n\n# #endregion AuthSchemas\n" }, { "contract_id": "Token", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 14, - "end_line": 21, + "start_line": 15, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50289,8 +54123,8 @@ "contract_id": "TokenData", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 24, - "end_line": 31, + "start_line": 25, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50307,8 +54141,8 @@ "contract_id": "PermissionSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 34, - "end_line": 45, + "start_line": 35, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50325,8 +54159,8 @@ "contract_id": "RoleSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 48, - "end_line": 60, + "start_line": 49, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50342,8 +54176,8 @@ "contract_id": "RoleCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 63, - "end_line": 71, + "start_line": 64, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50359,8 +54193,8 @@ "contract_id": "RoleUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 74, - "end_line": 82, + "start_line": 75, + "end_line": 83, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50376,8 +54210,8 @@ "contract_id": "ADGroupMappingSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 85, - "end_line": 96, + "start_line": 86, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50393,8 +54227,8 @@ "contract_id": "ADGroupMappingCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 99, - "end_line": 106, + "start_line": 100, + "end_line": 107, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50410,8 +54244,8 @@ "contract_id": "UserBase", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 109, - "end_line": 117, + "start_line": 110, + "end_line": 118, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50427,8 +54261,8 @@ "contract_id": "UserCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 120, - "end_line": 127, + "start_line": 121, + "end_line": 128, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50444,8 +54278,8 @@ "contract_id": "UserUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 130, - "end_line": 139, + "start_line": 131, + "end_line": 140, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50461,8 +54295,8 @@ "contract_id": "User", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 142, - "end_line": 155, + "start_line": 143, + "end_line": 156, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50955,11 +54789,12 @@ "contract_type": "Module", "file_path": "backend/src/schemas/profile.py", "start_line": 1, - "end_line": 191, - "tier": "TIER_2", - "complexity": 3, + "end_line": 192, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ProfilePayload -> ProfileSchema", "INVARIANT": "Schema shapes stay stable for profile UI states and backend preference contracts.", "LAYER": "API", "PURPOSE": "Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup." @@ -50974,25 +54809,43 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, profile, schema, superset, profile-permission-state]\n#\n# @BRIEF Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> pydantic\n#\n# @INVARIANT: Schema shapes stay stable for profile UI states and backend preference contracts.\n\nfrom datetime import datetime\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\n\n# #region ProfilePermissionState [C:3] [TYPE Class]\n# @BRIEF Represents one permission badge state for profile read-only security view.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePermissionState(BaseModel):\n key: str\n allowed: bool\n\n\n# #endregion ProfilePermissionState\n\n\n# #region ProfileSecuritySummary [C:3] [TYPE Class]\n# @BRIEF Read-only security and access snapshot for current user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfileSecuritySummary(BaseModel):\n read_only: bool = True\n auth_source: str | None = None\n current_role: str | None = None\n role_source: str | None = None\n roles: list[str] = Field(default_factory=list)\n permissions: list[ProfilePermissionState] = Field(default_factory=list)\n\n\n# #endregion ProfileSecuritySummary\n\n\n# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ProfilePreference\n\n\n# #region ProfilePreferenceUpdateRequest [C:3] [TYPE Class]\n# @BRIEF Request payload for updating current user's profile settings.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceUpdateRequest(BaseModel):\n superset_username: str | None = Field(\n default=None,\n description=\"Apache Superset username bound to current user profile.\",\n )\n show_only_my_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" can auto-apply profile filter in main context.',\n )\n show_only_slug_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" hides dashboards without slug by default.',\n )\n git_username: str | None = Field(\n default=None,\n description=\"Git author username used for commit signature.\",\n )\n git_email: str | None = Field(\n default=None,\n description=\"Git author email used for commit signature.\",\n )\n git_personal_access_token: str | None = Field(\n default=None,\n description=\"Personal Access Token value. Empty string clears existing token.\",\n )\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\", \"reports-logs\"] | None = Field(\n default=None,\n description=\"Preferred start page after login.\",\n )\n auto_open_task_drawer: bool | None = Field(\n default=None,\n description=\"Auto-open task drawer when long-running tasks start.\",\n )\n dashboards_table_density: Literal[\"compact\", \"comfortable\", \"free\"] | None = (\n Field(\n default=None,\n description=\"Preferred table density for dashboard listings.\",\n )\n )\n telegram_id: str | None = Field(\n default=None,\n description=\"Telegram ID for notifications.\",\n )\n email_address: str | None = Field(\n default=None,\n description=\"Email address for notifications (overrides system email).\",\n )\n notify_on_fail: bool | None = Field(\n default=None,\n description=\"Whether to send notifications on validation failure.\",\n )\n\n\n# #endregion ProfilePreferenceUpdateRequest\n\n\n# #region ProfilePreferenceResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for profile preference read/update endpoints.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceResponse(BaseModel):\n status: Literal[\"success\", \"error\"] = \"success\"\n message: str | None = None\n validation_errors: list[str] = Field(default_factory=list)\n preference: ProfilePreference\n security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary)\n\n\n# #endregion ProfilePreferenceResponse\n\n\n# #region SupersetAccountLookupRequest [C:3] [TYPE Class]\n# @BRIEF Query contract for Superset account lookup by selected environment.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupRequest(BaseModel):\n environment_id: str\n search: str | None = None\n page_index: int = Field(default=0, ge=0)\n page_size: int = Field(default=20, ge=1, le=100)\n sort_column: str = Field(default=\"username\")\n sort_order: str = Field(default=\"desc\")\n\n\n# #endregion SupersetAccountLookupRequest\n\n\n# #region SupersetAccountCandidate [C:3] [TYPE Class]\n# @BRIEF Canonical account candidate projected from Superset users payload.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountCandidate(BaseModel):\n environment_id: str\n username: str\n display_name: str | None = None\n email: str | None = None\n is_active: bool | None = None\n\n\n# #endregion SupersetAccountCandidate\n\n\n# #region SupersetAccountLookupResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for Superset account lookup (success or degraded mode).\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupResponse(BaseModel):\n status: Literal[\"success\", \"degraded\"]\n environment_id: str\n page_index: int = Field(ge=0)\n page_size: int = Field(ge=1, le=100)\n total: int = Field(ge=0)\n warning: str | None = None\n items: list[SupersetAccountCandidate] = Field(default_factory=list)\n\n\n# #endregion SupersetAccountLookupResponse\n\n# #endregion ProfileSchemas\n" + "body": "# #region ProfileSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, profile, schema, superset, profile-permission-state]\n#\n# @BRIEF Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> pydantic\n#\n# @INVARIANT: Schema shapes stay stable for profile UI states and backend preference contracts.\n# @DATA_CONTRACT: ProfilePayload -> ProfileSchema\n\nfrom datetime import datetime\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\n\n# #region ProfilePermissionState [C:3] [TYPE Class]\n# @BRIEF Represents one permission badge state for profile read-only security view.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePermissionState(BaseModel):\n key: str\n allowed: bool\n\n\n# #endregion ProfilePermissionState\n\n\n# #region ProfileSecuritySummary [C:3] [TYPE Class]\n# @BRIEF Read-only security and access snapshot for current user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfileSecuritySummary(BaseModel):\n read_only: bool = True\n auth_source: str | None = None\n current_role: str | None = None\n role_source: str | None = None\n roles: list[str] = Field(default_factory=list)\n permissions: list[ProfilePermissionState] = Field(default_factory=list)\n\n\n# #endregion ProfileSecuritySummary\n\n\n# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ProfilePreference\n\n\n# #region ProfilePreferenceUpdateRequest [C:3] [TYPE Class]\n# @BRIEF Request payload for updating current user's profile settings.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceUpdateRequest(BaseModel):\n superset_username: str | None = Field(\n default=None,\n description=\"Apache Superset username bound to current user profile.\",\n )\n show_only_my_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" can auto-apply profile filter in main context.',\n )\n show_only_slug_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" hides dashboards without slug by default.',\n )\n git_username: str | None = Field(\n default=None,\n description=\"Git author username used for commit signature.\",\n )\n git_email: str | None = Field(\n default=None,\n description=\"Git author email used for commit signature.\",\n )\n git_personal_access_token: str | None = Field(\n default=None,\n description=\"Personal Access Token value. Empty string clears existing token.\",\n )\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\", \"reports-logs\"] | None = Field(\n default=None,\n description=\"Preferred start page after login.\",\n )\n auto_open_task_drawer: bool | None = Field(\n default=None,\n description=\"Auto-open task drawer when long-running tasks start.\",\n )\n dashboards_table_density: Literal[\"compact\", \"comfortable\", \"free\"] | None = (\n Field(\n default=None,\n description=\"Preferred table density for dashboard listings.\",\n )\n )\n telegram_id: str | None = Field(\n default=None,\n description=\"Telegram ID for notifications.\",\n )\n email_address: str | None = Field(\n default=None,\n description=\"Email address for notifications (overrides system email).\",\n )\n notify_on_fail: bool | None = Field(\n default=None,\n description=\"Whether to send notifications on validation failure.\",\n )\n\n\n# #endregion ProfilePreferenceUpdateRequest\n\n\n# #region ProfilePreferenceResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for profile preference read/update endpoints.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceResponse(BaseModel):\n status: Literal[\"success\", \"error\"] = \"success\"\n message: str | None = None\n validation_errors: list[str] = Field(default_factory=list)\n preference: ProfilePreference\n security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary)\n\n\n# #endregion ProfilePreferenceResponse\n\n\n# #region SupersetAccountLookupRequest [C:3] [TYPE Class]\n# @BRIEF Query contract for Superset account lookup by selected environment.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupRequest(BaseModel):\n environment_id: str\n search: str | None = None\n page_index: int = Field(default=0, ge=0)\n page_size: int = Field(default=20, ge=1, le=100)\n sort_column: str = Field(default=\"username\")\n sort_order: str = Field(default=\"desc\")\n\n\n# #endregion SupersetAccountLookupRequest\n\n\n# #region SupersetAccountCandidate [C:3] [TYPE Class]\n# @BRIEF Canonical account candidate projected from Superset users payload.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountCandidate(BaseModel):\n environment_id: str\n username: str\n display_name: str | None = None\n email: str | None = None\n is_active: bool | None = None\n\n\n# #endregion SupersetAccountCandidate\n\n\n# #region SupersetAccountLookupResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for Superset account lookup (success or degraded mode).\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupResponse(BaseModel):\n status: Literal[\"success\", \"degraded\"]\n environment_id: str\n page_index: int = Field(ge=0)\n page_size: int = Field(ge=1, le=100)\n total: int = Field(ge=0)\n warning: str | None = None\n items: list[SupersetAccountCandidate] = Field(default_factory=list)\n\n\n# #endregion SupersetAccountLookupResponse\n\n# #endregion ProfileSchemas\n" }, { "contract_id": "ProfilePermissionState", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 15, - "end_line": 23, + "start_line": 16, + "end_line": 24, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51016,8 +54869,8 @@ "contract_id": "ProfileSecuritySummary", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 26, - "end_line": 38, + "start_line": 27, + "end_line": 39, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51041,8 +54894,8 @@ "contract_id": "ProfilePreference", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 41, - "end_line": 71, + "start_line": 42, + "end_line": 72, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51066,8 +54919,8 @@ "contract_id": "ProfilePreferenceUpdateRequest", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 74, - "end_line": 130, + "start_line": 75, + "end_line": 131, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51091,8 +54944,8 @@ "contract_id": "ProfilePreferenceResponse", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 133, - "end_line": 144, + "start_line": 134, + "end_line": 145, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51116,8 +54969,8 @@ "contract_id": "SupersetAccountLookupRequest", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 147, - "end_line": 159, + "start_line": 148, + "end_line": 160, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51141,8 +54994,8 @@ "contract_id": "SupersetAccountCandidate", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 162, - "end_line": 173, + "start_line": 163, + "end_line": 174, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -51166,8 +55019,8 @@ "contract_id": "SupersetAccountLookupResponse", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 176, - "end_line": 189, + "start_line": 177, + "end_line": 190, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -52598,11 +56451,12 @@ "contract_type": "Module", "file_path": "backend/src/scripts/clean_release_tui.py", "start_line": 1, - "end_line": 684, + "end_line": 780, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "DATA_CONTRACT": "CLIArgs -> TUIExitCode", "INVARIANT": "TUI refuses startup in non-TTY environments; headless flow is CLI/API only.", "LAYER": "UI", "PURPOSE": "Interactive terminal interface for Enterprise Clean Release compliance validation." @@ -52622,6 +56476,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", @@ -52634,14 +56497,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CleanReleaseTuiScript [C:3] [TYPE Module] [SEMANTICS clean-release, validate, compliance, release, tui-facade-adapter]\n# @BRIEF Interactive terminal interface for Enterprise Clean Release compliance validation.\n# @LAYER: UI\n# @RELATION DEPENDS_ON -> [ComplianceExecutionService]\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @INVARIANT: TUI refuses startup in non-TTY environments; headless flow is CLI/API only.\n\nimport curses\nimport json\nimport os\nimport sys\nfrom datetime import UTC, datetime\nfrom types import SimpleNamespace\nfrom typing import Any\n\n# Standardize sys.path for direct execution from project root or scripts dir.\nSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))\nBACKEND_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, \"..\", \"..\"))\nif BACKEND_ROOT not in sys.path:\n sys.path.insert(0, BACKEND_ROOT)\n\nfrom src.core.cot_logger import seed_trace_id\nfrom src.models.clean_release import (\n CandidateArtifact,\n CheckFinalStatus,\n CheckStageName,\n CheckStageStatus,\n CleanProfilePolicy,\n ComplianceViolation,\n ProfileType,\n RegistryStatus,\n ReleaseCandidate,\n ReleaseCandidateStatus,\n ResourceSourceEntry,\n ResourceSourceRegistry,\n)\nfrom src.services.clean_release.approval_service import approve_candidate\nfrom src.services.clean_release.artifact_catalog_loader import load_bootstrap_artifacts\nfrom src.services.clean_release.compliance_execution_service import (\n ComplianceExecutionService,\n)\nfrom src.services.clean_release.enums import CandidateStatus\nfrom src.services.clean_release.manifest_service import build_manifest_snapshot\nfrom src.services.clean_release.publication_service import publish_candidate\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region TuiFacadeAdapter [TYPE Class]\n# @BRIEF Thin TUI adapter that routes business mutations through application services.\n# @PRE: repository contains candidate and trusted policy/registry snapshots for execution.\n# @POST: Business actions return service results/errors without direct TUI-owned mutations.\nclass TuiFacadeAdapter:\n def __init__(self, repository: CleanReleaseRepository):\n self.repository = repository\n\n def _build_config_manager(self):\n policy = self.repository.get_active_policy()\n if policy is None:\n raise ValueError(\"Active policy not found\")\n clean_release = SimpleNamespace(\n active_policy_id=policy.id,\n active_registry_id=policy.registry_snapshot_id,\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n def run_compliance(self, *, candidate_id: str, actor: str):\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ValueError(\"Manifest required before compliance run\")\n latest_manifest = sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n service = ComplianceExecutionService(\n repository=self.repository,\n config_manager=self._build_config_manager(),\n )\n return service.execute_run(\n candidate_id=candidate_id,\n requested_by=actor,\n manifest_id=latest_manifest.id,\n )\n\n def approve_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for approval\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return approve_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n decided_by=actor,\n comment=\"Approved from TUI\",\n )\n\n def publish_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for publication\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return publish_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n published_by=actor,\n target_channel=\"stable\",\n publication_ref=None,\n )\n\n def build_manifest(self, *, candidate_id: str, actor: str):\n return build_manifest_snapshot(\n repository=self.repository,\n candidate_id=candidate_id,\n created_by=actor,\n )\n\n def get_overview(self, *, candidate_id: str) -> dict[str, Any]:\n candidate = self.repository.get_candidate(candidate_id)\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n runs = [\n item\n for item in self.repository.check_runs.values()\n if item.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(runs, key=lambda item: item.requested_at, reverse=True)[0]\n if runs\n else None\n )\n latest_report = next(\n (\n item\n for item in self.repository.reports.values()\n if latest_run and item.run_id == latest_run.id\n ),\n None,\n )\n approvals = getattr(self.repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approvals if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in approvals)\n else None\n )\n publications = getattr(self.repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publications if item.candidate_id == candidate_id],\n key=lambda item: item.published_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in publications)\n else None\n )\n policy = self.repository.get_active_policy()\n registry = (\n self.repository.get_registry(policy.internal_source_registry_ref)\n if policy\n else None\n )\n return {\n \"candidate\": candidate,\n \"manifest\": latest_manifest,\n \"run\": latest_run,\n \"report\": latest_report,\n \"approval\": latest_approval,\n \"publication\": latest_publication,\n \"policy\": policy,\n \"registry\": registry,\n }\n\n\n# #endregion TuiFacadeAdapter\n\n\n# #region CleanReleaseTUI [TYPE Class]\n# @BRIEF Curses-based application for compliance monitoring.\n# @UX_STATE: READY -> Waiting for operator to start checks (F5).\n# @UX_STATE: RUNNING -> Executing compliance stages with progress feedback.\n# @UX_STATE: COMPLIANT -> Release candidate passed all checks.\n# @UX_STATE: BLOCKED -> Violations detected, release forbidden.\n# @UX_FEEDBACK: Red alerts for BLOCKED status, Green for COMPLIANT.\nclass CleanReleaseTUI:\n def __init__(self, stdscr: curses.window):\n self.stdscr = stdscr\n self.mode = os.getenv(\"CLEAN_TUI_MODE\", \"demo\").strip().lower()\n self.repo = self._build_repository(self.mode)\n self.facade = TuiFacadeAdapter(self.repo)\n self.candidate_id = self._resolve_candidate_id()\n self.status: Any = \"READY\"\n self.checks_progress: list[dict[str, Any]] = []\n self.violations_list: list[ComplianceViolation] = []\n self.report_id: str | None = None\n self.last_error: str | None = None\n self.overview: dict[str, Any] = {}\n self.refresh_overview()\n\n curses.start_color()\n curses.use_default_colors()\n curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header/Footer\n curses.init_pair(2, curses.COLOR_GREEN, -1) # PASS\n curses.init_pair(3, curses.COLOR_RED, -1) # FAIL/BLOCKED\n curses.init_pair(4, curses.COLOR_YELLOW, -1) # RUNNING\n curses.init_pair(5, curses.COLOR_CYAN, -1) # Text\n\n def _build_repository(self, mode: str) -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n if mode == \"demo\":\n self._bootstrap_demo_repository(repo)\n else:\n self._bootstrap_real_repository(repo)\n return repo\n\n def _bootstrap_demo_repository(self, repository: CleanReleaseRepository) -> None:\n now = datetime.now(UTC)\n policy = CleanProfilePolicy(\n policy_id=\"POL-ENT-CLEAN\",\n policy_version=\"1\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"REG-1\",\n prohibited_artifact_categories=[\"test-data\"],\n effective_from=now,\n )\n policy.immutable = True\n repository.save_policy(policy)\n\n registry = ResourceSourceRegistry(\n registry_id=\"REG-1\",\n name=\"Default Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"S1\",\n host=\"internal-repo.company.com\",\n protocol=\"https\",\n purpose=\"artifactory\",\n )\n ],\n updated_at=now,\n updated_by=\"system\",\n )\n registry.immutable = True\n registry.allowed_hosts = [\"internal-repo.company.com\"]\n registry.allowed_schemes = [\"https\"]\n registry.allowed_source_types = [\"artifactory\"]\n repository.save_registry(registry)\n candidate = ReleaseCandidate(\n id=\"2026.03.03-rc1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"v1.0.0-rc1\",\n created_at=now,\n created_by=\"system\",\n status=CandidateStatus.DRAFT.value,\n )\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-1\",\n candidate_id=candidate.id,\n path=\"src/main.py\",\n sha256=\"sha256-demo-core\",\n size=128,\n detected_category=\"core\",\n )\n )\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-2\",\n candidate_id=candidate.id,\n path=\"test/data.csv\",\n sha256=\"sha256-demo-test\",\n size=64,\n detected_category=\"test-data\",\n )\n )\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate.id,\n created_by=\"system\",\n policy_id=\"POL-ENT-CLEAN\",\n )\n summary = dict(manifest.content_json.get(\"summary\", {}))\n summary[\"prohibited_detected_count\"] = 1\n manifest.content_json[\"summary\"] = summary\n\n def _bootstrap_real_repository(self, repository: CleanReleaseRepository) -> None:\n bootstrap_path = os.getenv(\"CLEAN_TUI_BOOTSTRAP_JSON\", \"\").strip()\n if not bootstrap_path:\n return\n\n with open(bootstrap_path, encoding=\"utf-8\") as bootstrap_file:\n payload = json.load(bootstrap_file)\n\n now = datetime.now(UTC)\n candidate = ReleaseCandidate(\n id=payload.get(\"candidate_id\", \"candidate-1\"),\n version=payload.get(\"version\", \"1.0.0\"),\n source_snapshot_ref=payload.get(\"source_snapshot_ref\", \"snapshot-ref\"),\n created_at=now,\n created_by=payload.get(\"created_by\", \"operator\"),\n status=ReleaseCandidateStatus.DRAFT,\n )\n repository.save_candidate(candidate)\n imported_artifacts = load_bootstrap_artifacts(\n os.getenv(\"CLEAN_TUI_ARTIFACTS_JSON\", \"\").strip(),\n candidate.id,\n )\n for artifact in imported_artifacts:\n repository.save_artifact(artifact)\n if imported_artifacts:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n registry_id = payload.get(\"registry_id\", \"REG-1\")\n entries = [\n ResourceSourceEntry(\n source_id=f\"S-{index + 1}\",\n host=host,\n protocol=\"https\",\n purpose=\"bootstrap\",\n enabled=True,\n )\n for index, host in enumerate(payload.get(\"allowed_hosts\", []))\n if str(host).strip()\n ]\n if entries:\n repository.save_registry(\n ResourceSourceRegistry(\n registry_id=registry_id,\n name=payload.get(\"registry_name\", \"Bootstrap Internal Registry\"),\n entries=entries,\n updated_at=now,\n updated_by=payload.get(\"created_by\", \"operator\"),\n status=RegistryStatus.ACTIVE,\n )\n )\n\n if entries:\n repository.save_policy(\n CleanProfilePolicy(\n policy_id=payload.get(\"policy_id\", \"POL-ENT-CLEAN\"),\n policy_version=payload.get(\"policy_version\", \"1\"),\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=registry_id,\n prohibited_artifact_categories=payload.get(\n \"prohibited_artifact_categories\",\n [\"test-data\", \"demo\", \"load-test\"],\n ),\n required_system_categories=payload.get(\n \"required_system_categories\", [\"core\"]\n ),\n effective_from=now,\n )\n )\n\n def _resolve_candidate_id(self) -> str:\n env_candidate = os.getenv(\"CLEAN_TUI_CANDIDATE_ID\", \"\").strip()\n if env_candidate:\n return env_candidate\n\n candidate_ids = list(self.repo.candidates.keys())\n if candidate_ids:\n return candidate_ids[0]\n return \"\"\n\n def draw_header(self, max_y: int, max_x: int):\n header_text = \" Enterprise Clean Release Validator (TUI) \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n # Avoid slicing if possible to satisfy Pyre, or use explicit int\n centered = header_text.center(max_x)\n self.stdscr.addstr(0, 0, centered[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n\n candidate = self.overview.get(\"candidate\")\n candidate_text = self.candidate_id or \"not-set\"\n profile_text = \"enterprise-clean\"\n lifecycle = getattr(candidate, \"status\", \"UNKNOWN\")\n info_line_text = (\n f\" │ Candidate: [{candidate_text}] Profile: [{profile_text}] \"\n f\"Lifecycle: [{lifecycle}] Mode: [{self.mode}]\"\n ).ljust(max_x)\n self.stdscr.addstr(2, 0, info_line_text[:max_x])\n\n def draw_checks(self):\n self.stdscr.addstr(4, 3, \"Checks:\")\n check_defs = [\n (CheckStageName.DATA_PURITY, \"Data Purity (no test/demo payloads)\"),\n (\n CheckStageName.INTERNAL_SOURCES_ONLY,\n \"Internal Sources Only (company servers)\",\n ),\n (CheckStageName.NO_EXTERNAL_ENDPOINTS, \"No External Internet Endpoints\"),\n (CheckStageName.MANIFEST_CONSISTENCY, \"Release Manifest Consistency\"),\n ]\n\n row = 5\n drawn_checks = {c[\"stage\"]: c for c in self.checks_progress}\n\n for stage, desc in check_defs:\n status_text = \" \"\n color = curses.color_pair(5)\n\n if stage in drawn_checks:\n c = drawn_checks[stage]\n if c[\"status\"] == \"RUNNING\":\n status_text = \"...\"\n color = curses.color_pair(4)\n elif c[\"status\"] == CheckStageStatus.PASS:\n status_text = \"PASS\"\n color = curses.color_pair(2)\n elif c[\"status\"] == CheckStageStatus.FAIL:\n status_text = \"FAIL\"\n color = curses.color_pair(3)\n\n self.stdscr.addstr(row, 4, f\"[{status_text:^4}] {desc}\")\n if status_text != \" \":\n self.stdscr.addstr(row, 50, f\"{status_text:>10}\", color | curses.A_BOLD)\n row += 1\n\n def draw_sources(self):\n self.stdscr.addstr(12, 3, \"Allowed Internal Sources:\", curses.A_BOLD)\n reg = self.overview.get(\"registry\")\n row = 13\n if reg:\n for entry in reg.entries:\n self.stdscr.addstr(row, 3, f\" - {entry.host}\")\n row += 1\n else:\n self.stdscr.addstr(row, 3, \" - (none)\")\n\n def draw_status(self):\n color = curses.color_pair(5)\n if self.status == CheckFinalStatus.COMPLIANT:\n color = curses.color_pair(2)\n elif self.status == CheckFinalStatus.BLOCKED:\n color = curses.color_pair(3)\n\n stat_str = str(\n self.status.value if hasattr(self.status, \"value\") else self.status\n )\n self.stdscr.addstr(\n 18, 3, f\"FINAL STATUS: {stat_str.upper()}\", color | curses.A_BOLD\n )\n\n if self.report_id:\n self.stdscr.addstr(19, 3, f\"Report ID: {self.report_id}\")\n\n approval = self.overview.get(\"approval\")\n publication = self.overview.get(\"publication\")\n if approval:\n self.stdscr.addstr(20, 3, f\"Approval: {approval.decision}\")\n if publication:\n self.stdscr.addstr(20, 32, f\"Publication: {publication.status}\")\n\n if self.violations_list:\n self.stdscr.addstr(\n 21,\n 3,\n f\"Violations Details ({len(self.violations_list)} total):\",\n curses.color_pair(3) | curses.A_BOLD,\n )\n row = 22\n for i, v in enumerate(self.violations_list[:5]):\n v_cat = str(getattr(v, \"code\", \"VIOLATION\"))\n msg = str(getattr(v, \"message\", \"Violation detected\"))\n location = str(\n getattr(v, \"artifact_path\", \"\")\n or getattr(getattr(v, \"evidence_json\", {}), \"get\", lambda *_: \"\")(\n \"location\", \"\"\n )\n )\n msg_text = f\"[{v_cat}] {msg} (Loc: {location})\"\n self.stdscr.addstr(row + i, 5, msg_text[:70], curses.color_pair(3))\n if self.last_error:\n self.stdscr.addstr(\n 27,\n 3,\n f\"Error: {self.last_error}\"[:100],\n curses.color_pair(3) | curses.A_BOLD,\n )\n\n def draw_footer(self, max_y: int, max_x: int):\n footer_text = \" F5 Run F6 Manifest F7 Refresh F8 Approve F9 Publish F10 Exit \".center(\n max_x\n )\n self.stdscr.attron(curses.color_pair(1))\n self.stdscr.addstr(max_y - 1, 0, footer_text[:max_x])\n self.stdscr.attroff(curses.color_pair(1))\n\n # [DEF:run_checks:Function]\n # @PURPOSE: Execute compliance run via facade adapter and update UI state.\n # @PRE: Candidate and policy snapshots are present in repository.\n # @POST: UI reflects final run/report/violation state from service result.\n def run_checks(self):\n self.status = \"RUNNING\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_screen()\n\n try:\n result = self.facade.run_compliance(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n except Exception as exc:\n self.status = CheckFinalStatus.FAILED\n self.last_error = str(exc)\n self.refresh_screen()\n return\n\n self.checks_progress = [\n {\n \"stage\": stage.stage_name,\n \"status\": CheckStageStatus.PASS\n if str(stage.decision).upper() == \"PASSED\"\n else CheckStageStatus.FAIL,\n }\n for stage in result.stage_runs\n ]\n self.violations_list = result.violations\n self.report_id = result.report.id if result.report is not None else None\n\n final_status = str(result.run.final_status or \"\").upper()\n if final_status in {\"BLOCKED\", CheckFinalStatus.BLOCKED.value}:\n self.status = CheckFinalStatus.BLOCKED\n elif final_status in {\"COMPLIANT\", \"PASSED\", CheckFinalStatus.COMPLIANT.value}:\n self.status = CheckFinalStatus.COMPLIANT\n else:\n self.status = CheckFinalStatus.FAILED\n self.refresh_overview()\n self.refresh_screen()\n\n # [/DEF:run_checks:Function]\n\n def build_manifest(self):\n try:\n manifest = self.facade.build_manifest(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = f\"Manifest built: {manifest.id}\"\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def clear_history(self):\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_overview()\n self.refresh_screen()\n\n def approve_latest(self):\n if not self.report_id:\n self.last_error = \"F8 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.approve_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def publish_latest(self):\n if not self.report_id:\n self.last_error = \"F9 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.publish_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def refresh_overview(self):\n if not self.report_id:\n self.last_error = \"F9 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.publish_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def refresh_overview(self):\n if not self.candidate_id:\n self.overview = {}\n return\n self.overview = self.facade.get_overview(candidate_id=self.candidate_id)\n\n def refresh_screen(self):\n max_y, max_x = self.stdscr.getmaxyx()\n self.stdscr.clear()\n try:\n self.draw_header(max_y, max_x)\n self.draw_checks()\n self.draw_sources()\n self.draw_status()\n self.draw_footer(max_y, max_x)\n except Exception:\n pass\n self.stdscr.refresh()\n\n def loop(self):\n self.refresh_screen()\n while True:\n char = self.stdscr.getch()\n if char == curses.KEY_F10:\n break\n elif char == curses.KEY_F5:\n self.run_checks()\n elif char == curses.KEY_F6:\n self.build_manifest()\n elif char == curses.KEY_F7:\n self.clear_history()\n elif char == curses.KEY_F8:\n self.approve_latest()\n elif char == curses.KEY_F9:\n self.publish_latest()\n\n\n# #endregion CleanReleaseTUI\n\n\ndef tui_main(stdscr: curses.window):\n curses.curs_set(0) # Hide cursor\n app = CleanReleaseTUI(stdscr)\n app.loop()\n\n\ndef main() -> int:\n seed_trace_id()\n # TUI requires interactive terminal; headless mode must use CLI/API flow.\n if not sys.stdout.isatty():\n print(\n \"TTY is required for TUI mode. Use CLI/API workflow instead.\",\n file=sys.stderr,\n )\n return 2\n try:\n curses.wrapper(tui_main)\n return 0\n except Exception as e:\n print(f\"Error starting TUI: {e}\", file=sys.stderr)\n return 1\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n# #endregion CleanReleaseTuiScript\n" + "body": "# #region CleanReleaseTuiScript [C:3] [TYPE Module] [SEMANTICS clean-release, validate, compliance, release, tui-facade-adapter]\n# @BRIEF Interactive terminal interface for Enterprise Clean Release compliance validation.\n# @LAYER: UI\n# @RELATION DEPENDS_ON -> [ComplianceExecutionService]\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @INVARIANT: TUI refuses startup in non-TTY environments; headless flow is CLI/API only.\n# @DATA_CONTRACT: CLIArgs -> TUIExitCode\nimport contextlib\nimport curses\nimport json\nimport os\nimport subprocess\nimport sys\nimport threading\nfrom datetime import UTC, datetime\nfrom types import SimpleNamespace\nfrom typing import Any\n# Standardize sys.path for direct execution from project root or scripts dir.\nSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))\nBACKEND_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, \"..\", \"..\"))\nif BACKEND_ROOT not in sys.path:\n sys.path.insert(0, BACKEND_ROOT)\nfrom src.core.cot_logger import seed_trace_id\nfrom src.models.clean_release import (\n CandidateArtifact,\n CheckFinalStatus,\n CheckStageName,\n CheckStageStatus,\n CleanProfilePolicy,\n ComplianceViolation,\n ProfileType,\n RegistryStatus,\n ReleaseCandidate,\n ReleaseCandidateStatus,\n ResourceSourceEntry,\n ResourceSourceRegistry,\n)\nfrom src.services.clean_release.approval_service import approve_candidate\nfrom src.services.clean_release.artifact_catalog_loader import load_bootstrap_artifacts\nfrom src.services.clean_release.compliance_execution_service import (\n ComplianceExecutionService,\n)\nfrom src.services.clean_release.enums import CandidateStatus\nfrom src.services.clean_release.manifest_service import build_manifest_snapshot\nfrom src.services.clean_release.publication_service import publish_candidate\nfrom src.services.clean_release.repository import CleanReleaseRepository\n# #region TuiFacadeAdapter [TYPE Class]\n# @BRIEF Thin TUI adapter that routes business mutations through application services.\n# @PRE: repository contains candidate and trusted policy/registry snapshots for execution.\n# @POST: Business actions return service results/errors without direct TUI-owned mutations.\nclass TuiFacadeAdapter:\n def __init__(self, repository: CleanReleaseRepository):\n self.repository = repository\n def _build_config_manager(self):\n policy = self.repository.get_active_policy()\n if policy is None:\n raise ValueError(\"Active policy not found\")\n clean_release = SimpleNamespace(\n active_policy_id=policy.id,\n active_registry_id=policy.registry_snapshot_id,\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n def run_compliance(self, *, candidate_id: str, actor: str):\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ValueError(\"Manifest required before compliance run\")\n latest_manifest = sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n service = ComplianceExecutionService(\n repository=self.repository,\n config_manager=self._build_config_manager(),\n )\n return service.execute_run(\n candidate_id=candidate_id,\n requested_by=actor,\n manifest_id=latest_manifest.id,\n )\n def approve_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for approval\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return approve_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n decided_by=actor,\n comment=\"Approved from TUI\",\n )\n def publish_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for publication\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return publish_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n published_by=actor,\n target_channel=\"stable\",\n publication_ref=None,\n )\n def build_manifest(self, *, candidate_id: str, actor: str):\n return build_manifest_snapshot(\n repository=self.repository,\n candidate_id=candidate_id,\n created_by=actor,\n )\n def get_overview(self, *, candidate_id: str) -> dict[str, Any]:\n candidate = self.repository.get_candidate(candidate_id)\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n runs = [\n item\n for item in self.repository.check_runs.values()\n if item.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(runs, key=lambda item: item.requested_at, reverse=True)[0]\n if runs\n else None\n )\n latest_report = next(\n (\n item\n for item in self.repository.reports.values()\n if latest_run and item.run_id == latest_run.id\n ),\n None,\n )\n approvals = getattr(self.repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approvals if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in approvals)\n else None\n )\n publications = getattr(self.repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publications if item.candidate_id == candidate_id],\n key=lambda item: item.published_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in publications)\n else None\n )\n policy = self.repository.get_active_policy()\n registry = (\n self.repository.get_registry(policy.internal_source_registry_ref)\n if policy\n else None\n )\n return {\n \"candidate\": candidate,\n \"manifest\": latest_manifest,\n \"run\": latest_run,\n \"report\": latest_report,\n \"approval\": latest_approval,\n \"publication\": latest_publication,\n \"policy\": policy,\n \"registry\": registry,\n }\n# #endregion TuiFacadeAdapter\n# #region CleanReleaseTUI [TYPE Class]\n# @BRIEF Curses-based application for compliance monitoring.\n# @UX_STATE: READY -> Waiting for operator to start checks (F5).\n# @UX_STATE: RUNNING -> Executing compliance stages with progress feedback.\n# @UX_STATE: COMPLIANT -> Release candidate passed all checks.\n# @UX_STATE: BLOCKED -> Violations detected, release forbidden.\n# @UX_FEEDBACK: Red alerts for BLOCKED status, Green for COMPLIANT.\nclass CleanReleaseTUI:\n def __init__(self, stdscr: curses.window):\n self.stdscr = stdscr\n self.mode = os.getenv(\"CLEAN_TUI_MODE\", \"demo\").strip().lower()\n self.repo = self._build_repository(self.mode)\n self.facade = TuiFacadeAdapter(self.repo)\n self.candidate_id = self._resolve_candidate_id()\n self.status: Any = \"READY\"\n self.checks_progress: list[dict[str, Any]] = []\n self.violations_list: list[ComplianceViolation] = []\n self.report_id: str | None = None\n self.last_error: str | None = None\n self.overview: dict[str, Any] = {}\n self.refresh_overview()\n curses.start_color()\n curses.use_default_colors()\n curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header/Footer\n curses.init_pair(2, curses.COLOR_GREEN, -1) # PASS\n curses.init_pair(3, curses.COLOR_RED, -1) # FAIL/BLOCKED\n curses.init_pair(4, curses.COLOR_YELLOW, -1) # RUNNING\n curses.init_pair(5, curses.COLOR_CYAN, -1) # Text\n def _build_repository(self, mode: str) -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n if mode == \"demo\":\n self._bootstrap_demo_repository(repo)\n else:\n self._bootstrap_real_repository(repo)\n return repo\n def _bootstrap_demo_repository(self, repository: CleanReleaseRepository) -> None:\n now = datetime.now(UTC)\n policy = CleanProfilePolicy(\n policy_id=\"POL-ENT-CLEAN\",\n policy_version=\"1\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"REG-1\",\n prohibited_artifact_categories=[\"test-data\"],\n effective_from=now,\n )\n policy.immutable = True\n repository.save_policy(policy)\n registry = ResourceSourceRegistry(\n registry_id=\"REG-1\",\n name=\"Default Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"S1\",\n host=\"internal-repo.company.com\",\n protocol=\"https\",\n purpose=\"artifactory\",\n )\n ],\n updated_at=now,\n updated_by=\"system\",\n )\n registry.immutable = True\n registry.allowed_hosts = [\"internal-repo.company.com\"]\n registry.allowed_schemes = [\"https\"]\n registry.allowed_source_types = [\"artifactory\"]\n repository.save_registry(registry)\n candidate = ReleaseCandidate(\n id=\"2026.03.03-rc1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"v1.0.0-rc1\",\n created_at=now,\n created_by=\"system\",\n status=CandidateStatus.DRAFT.value,\n )\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-1\",\n candidate_id=candidate.id,\n path=\"src/main.py\",\n sha256=\"sha256-demo-core\",\n size=128,\n detected_category=\"core\",\n )\n )\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-2\",\n candidate_id=candidate.id,\n path=\"test/data.csv\",\n sha256=\"sha256-demo-test\",\n size=64,\n detected_category=\"test-data\",\n )\n )\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate.id,\n created_by=\"system\",\n policy_id=\"POL-ENT-CLEAN\",\n )\n summary = dict(manifest.content_json.get(\"summary\", {}))\n summary[\"prohibited_detected_count\"] = 1\n manifest.content_json[\"summary\"] = summary\n def _bootstrap_real_repository(self, repository: CleanReleaseRepository) -> None:\n bootstrap_path = os.getenv(\"CLEAN_TUI_BOOTSTRAP_JSON\", \"\").strip()\n if not bootstrap_path:\n return\n with open(bootstrap_path, encoding=\"utf-8\") as bootstrap_file:\n payload = json.load(bootstrap_file)\n now = datetime.now(UTC)\n candidate = ReleaseCandidate(\n id=payload.get(\"candidate_id\", \"candidate-1\"),\n version=payload.get(\"version\", \"1.0.0\"),\n source_snapshot_ref=payload.get(\"source_snapshot_ref\", \"snapshot-ref\"),\n created_at=now,\n created_by=payload.get(\"created_by\", \"operator\"),\n status=ReleaseCandidateStatus.DRAFT,\n )\n repository.save_candidate(candidate)\n imported_artifacts = load_bootstrap_artifacts(\n os.getenv(\"CLEAN_TUI_ARTIFACTS_JSON\", \"\").strip(),\n candidate.id,\n )\n for artifact in imported_artifacts:\n repository.save_artifact(artifact)\n if imported_artifacts:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n registry_id = payload.get(\"registry_id\", \"REG-1\")\n entries = [\n ResourceSourceEntry(\n source_id=f\"S-{index + 1}\",\n host=host,\n protocol=\"https\",\n purpose=\"bootstrap\",\n enabled=True,\n )\n for index, host in enumerate(payload.get(\"allowed_hosts\", []))\n if str(host).strip()\n ]\n if entries:\n repository.save_registry(\n ResourceSourceRegistry(\n registry_id=registry_id,\n name=payload.get(\"registry_name\", \"Bootstrap Internal Registry\"),\n entries=entries,\n updated_at=now,\n updated_by=payload.get(\"created_by\", \"operator\"),\n status=RegistryStatus.ACTIVE,\n )\n )\n if entries:\n repository.save_policy(\n CleanProfilePolicy(\n policy_id=payload.get(\"policy_id\", \"POL-ENT-CLEAN\"),\n policy_version=payload.get(\"policy_version\", \"1\"),\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=registry_id,\n prohibited_artifact_categories=payload.get(\n \"prohibited_artifact_categories\",\n [\"test-data\", \"demo\", \"load-test\"],\n ),\n required_system_categories=payload.get(\n \"required_system_categories\", [\"core\"]\n ),\n effective_from=now,\n )\n )\n def _resolve_candidate_id(self) -> str:\n env_candidate = os.getenv(\"CLEAN_TUI_CANDIDATE_ID\", \"\").strip()\n if env_candidate:\n return env_candidate\n candidate_ids = list(self.repo.candidates.keys())\n if candidate_ids:\n return candidate_ids[0]\n return \"\"\n def draw_header(self, max_y: int, max_x: int):\n header_text = \" Enterprise Clean Release Validator (TUI) \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n # Avoid slicing if possible to satisfy Pyre, or use explicit int\n centered = header_text.center(max_x)\n self.stdscr.addstr(0, 0, centered[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n candidate = self.overview.get(\"candidate\")\n candidate_text = self.candidate_id or \"not-set\"\n profile_text = \"enterprise-clean\"\n lifecycle = getattr(candidate, \"status\", \"UNKNOWN\")\n info_line_text = (\n f\" │ Candidate: [{candidate_text}] Profile: [{profile_text}] \"\n f\"Lifecycle: [{lifecycle}] Mode: [{self.mode}]\"\n ).ljust(max_x)\n self.stdscr.addstr(2, 0, info_line_text[:max_x])\n def draw_checks(self):\n self.stdscr.addstr(4, 3, \"Checks:\")\n check_defs = [\n (CheckStageName.DATA_PURITY, \"Data Purity (no test/demo payloads)\"),\n (\n CheckStageName.INTERNAL_SOURCES_ONLY,\n \"Internal Sources Only (company servers)\",\n ),\n (CheckStageName.NO_EXTERNAL_ENDPOINTS, \"No External Internet Endpoints\"),\n (CheckStageName.MANIFEST_CONSISTENCY, \"Release Manifest Consistency\"),\n ]\n row = 5\n drawn_checks = {c[\"stage\"]: c for c in self.checks_progress}\n for stage, desc in check_defs:\n status_text = \" \"\n color = curses.color_pair(5)\n if stage in drawn_checks:\n c = drawn_checks[stage]\n if c[\"status\"] == \"RUNNING\":\n status_text = \"...\"\n color = curses.color_pair(4)\n elif c[\"status\"] == CheckStageStatus.PASS:\n status_text = \"PASS\"\n color = curses.color_pair(2)\n elif c[\"status\"] == CheckStageStatus.FAIL:\n status_text = \"FAIL\"\n color = curses.color_pair(3)\n self.stdscr.addstr(row, 4, f\"[{status_text:^4}] {desc}\")\n if status_text != \" \":\n self.stdscr.addstr(row, 50, f\"{status_text:>10}\", color | curses.A_BOLD)\n row += 1\n def draw_sources(self):\n self.stdscr.addstr(12, 3, \"Allowed Internal Sources:\", curses.A_BOLD)\n reg = self.overview.get(\"registry\")\n row = 13\n if reg:\n for entry in reg.entries:\n self.stdscr.addstr(row, 3, f\" - {entry.host}\")\n row += 1\n else:\n self.stdscr.addstr(row, 3, \" - (none)\")\n def draw_status(self):\n color = curses.color_pair(5)\n if self.status == CheckFinalStatus.COMPLIANT:\n color = curses.color_pair(2)\n elif self.status == CheckFinalStatus.BLOCKED:\n color = curses.color_pair(3)\n stat_str = str(\n self.status.value if hasattr(self.status, \"value\") else self.status\n )\n self.stdscr.addstr(\n 18, 3, f\"FINAL STATUS: {stat_str.upper()}\", color | curses.A_BOLD\n )\n if self.report_id:\n self.stdscr.addstr(19, 3, f\"Report ID: {self.report_id}\")\n approval = self.overview.get(\"approval\")\n publication = self.overview.get(\"publication\")\n if approval:\n self.stdscr.addstr(20, 3, f\"Approval: {approval.decision}\")\n if publication:\n self.stdscr.addstr(20, 32, f\"Publication: {publication.status}\")\n if self.violations_list:\n self.stdscr.addstr(\n 21,\n 3,\n f\"Violations Details ({len(self.violations_list)} total):\",\n curses.color_pair(3) | curses.A_BOLD,\n )\n row = 22\n for i, v in enumerate(self.violations_list[:5]):\n v_cat = str(getattr(v, \"code\", \"VIOLATION\"))\n msg = str(getattr(v, \"message\", \"Violation detected\"))\n location = str(\n getattr(v, \"artifact_path\", \"\")\n or getattr(getattr(v, \"evidence_json\", {}), \"get\", lambda *_: \"\")(\n \"location\", \"\"\n )\n )\n msg_text = f\"[{v_cat}] {msg} (Loc: {location})\"\n self.stdscr.addstr(row + i, 5, msg_text[:70], curses.color_pair(3))\n if self.last_error:\n self.stdscr.addstr(\n 27,\n 3,\n f\"Error: {self.last_error}\"[:100],\n curses.color_pair(3) | curses.A_BOLD,\n )\n def draw_footer(self, max_y: int, max_x: int):\n footer_text = \" F4 Build Bundle F5 Run F6 Manifest F7 Refresh F8 Approve F9 Publish F10 Exit \".center(\n max_x\n )\n self.stdscr.attron(curses.color_pair(1))\n self.stdscr.addstr(max_y - 1, 0, footer_text[:max_x])\n self.stdscr.attroff(curses.color_pair(1))\n # [DEF:run_checks:Function]\n # @PURPOSE: Execute compliance run via facade adapter and update UI state.\n # @PRE: Candidate and policy snapshots are present in repository.\n # @POST: UI reflects final run/report/violation state from service result.\n def run_checks(self):\n self.status = \"RUNNING\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_screen()\n try:\n result = self.facade.run_compliance(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n except Exception as exc:\n self.status = CheckFinalStatus.FAILED\n self.last_error = str(exc)\n self.refresh_screen()\n return\n self.checks_progress = [\n {\n \"stage\": stage.stage_name,\n \"status\": CheckStageStatus.PASS\n if str(stage.decision).upper() == \"PASSED\"\n else CheckStageStatus.FAIL,\n }\n for stage in result.stage_runs\n ]\n self.violations_list = result.violations\n self.report_id = result.report.id if result.report is not None else None\n final_status = str(result.run.final_status or \"\").upper()\n if final_status in {\"BLOCKED\", CheckFinalStatus.BLOCKED.value}:\n self.status = CheckFinalStatus.BLOCKED\n elif final_status in {\"COMPLIANT\", \"PASSED\", CheckFinalStatus.COMPLIANT.value}:\n self.status = CheckFinalStatus.COMPLIANT\n else:\n self.status = CheckFinalStatus.FAILED\n self.refresh_overview()\n self.refresh_screen()\n # [/DEF:run_checks:Function]\n def build_manifest(self):\n try:\n manifest = self.facade.build_manifest(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = f\"Manifest built: {manifest.id}\"\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n def clear_history(self):\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_overview()\n self.refresh_screen()\n def approve_latest(self):\n if not self.report_id:\n self.last_error = \"F8 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.approve_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n def publish_latest(self):\n if not self.report_id:\n self.last_error = \"F9 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.publish_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n # #region bundle_build_mode [C:4] [TYPE Function] [SEMANTICS bundle,build,release]\n # @BRIEF Interactive bundle build screen — select type, enter tag, watch live build output.\n # @PRE TTY is available. Bundle type selected (1 or 2). Tag is non-empty.\n # @POST Subprocess completes (success/failure). Output displayed. User returns via Esc.\n # @SIDE_EFFECT Runs docker build subprocess, creates dist/docker/ files\n # @RELATION CALLS -> [EXT:build.sh]\n # @UX_STATE BUNDLE_MENU -> User selects bundle type and enters release tag\n # @UX_STATE BUNDLE_BUILDING -> Subprocess running, real-time output displayed\n # @UX_STATE BUNDLE_DONE -> Build completed with success/failure result\n def bundle_build_mode(self):\n \"\"\"Enter bundle build sub-loop: choose type, enter tag, watch live output.\"\"\"\n max_y, max_x = self.stdscr.getmaxyx()\n # ── Phase 1: Bundle type selection ──\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, \" Bundle Build \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, \"Select bundle type:\", curses.A_BOLD)\n self.stdscr.addstr(5, 5, \"[1] Full Bundle (backend + frontend + postgres — all dependencies)\")\n self.stdscr.addstr(6, 5, \"[2] Light Bundle (~200MB, compressed without Playwright)\")\n self.stdscr.addstr(8, 3, \"Your choice: \")\n self.stdscr.addstr(max_y - 1, 0, \" Esc: Cancel \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n bundle_cmd: str | None = None\n bundle_label: str | None = None\n while True:\n k = self.stdscr.getch()\n if k == ord(\"1\"):\n bundle_cmd, bundle_label = \"bundle\", \"Full Bundle\"\n break\n if k == ord(\"2\"):\n bundle_cmd, bundle_label = \"bundle:light\", \"Light Bundle\"\n break\n if k == 27: # Esc\n return\n # ── Phase 2: Tag input ──\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, f\" Bundle Build — {bundle_label} \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, f\"Type: {bundle_label}\", curses.A_BOLD)\n self.stdscr.addstr(5, 3, \"Release tag (e.g. v1.0.0): \")\n self.stdscr.addstr(max_y - 1, 0, \" Esc: Cancel \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n curses.echo()\n raw_tag = self.stdscr.getstr(5, 33, 30).decode().strip()\n curses.noecho()\n tag = raw_tag.strip()\n if not tag:\n return\n # ── Phase 3: Build execution ──\n build_script = os.path.join(os.path.dirname(BACKEND_ROOT), \"build.sh\")\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, f\" Building {bundle_label} — {tag} \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.refresh()\n try:\n proc = subprocess.Popen(\n [build_script, bundle_cmd, tag],\n stdout=subprocess.PIPE,\n stderr=subprocess.STDOUT,\n text=True,\n bufsize=1,\n )\n except (FileNotFoundError, OSError) as exc:\n self.stdscr.addstr(3, 3, str(exc), curses.color_pair(3))\n self.stdscr.addstr(max_y - 1, 0, \" Press Esc to return \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n while self.stdscr.getch() != 27:\n pass\n return\n output_lines: list[str] = []\n lock = threading.Lock()\n build_done = threading.Event()\n return_code: list[int | None] = [None]\n def _reader_thread() -> None:\n \"\"\"Read subprocess stdout line by line in a background thread.\"\"\"\n try:\n for line in iter(proc.stdout.readline, \"\"):\n with lock:\n output_lines.append(line.rstrip(\"\\n\"))\n proc.stdout.close()\n except ValueError:\n pass\n return_code[0] = proc.wait()\n build_done.set()\n reader = threading.Thread(target=_reader_thread, daemon=True)\n reader.start()\n output_top = 2\n output_height = max_y - 4\n canceled = False\n while not build_done.is_set():\n with lock:\n visible = output_lines[-(output_height - 1):] if output_lines else []\n for i, line in enumerate(visible):\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(output_top + i, 2, line[:max_x - 4])\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(max_y - 2, 2, \" Building... (Esc to cancel) \",\n curses.color_pair(4))\n self.stdscr.refresh()\n self.stdscr.timeout(200)\n k = self.stdscr.getch()\n self.stdscr.timeout(-1)\n if k == 27: # Esc\n proc.terminate()\n canceled = True\n build_done.wait(timeout=5.0)\n if proc.poll() is None:\n proc.kill()\n build_done.wait(timeout=2.0)\n break\n if not canceled:\n build_done.wait()\n try:\n remaining = proc.stdout.read()\n if remaining:\n with lock:\n for line in remaining.rstrip(\"\\n\").split(\"\\n\"):\n if line:\n output_lines.append(line)\n except Exception:\n pass\n # ── Phase 4: Result summary ──\n self.stdscr.clear()\n ret = return_code[0]\n if ret == 0:\n title = f\" ✅ BUILD COMPLETE — {bundle_label} / {tag} \"\n elif canceled:\n title = f\" ⏹ BUILD CANCELED — {bundle_label} / {tag} \"\n else:\n title = f\" ❌ BUILD FAILED — {bundle_label} / {tag} \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, title.center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n if ret == 0:\n self.stdscr.addstr(2, 3, \"Build completed successfully!\",\n curses.color_pair(2) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, \"Output: dist/docker/\",\n curses.color_pair(2))\n elif canceled:\n self.stdscr.addstr(2, 3, \"Build canceled by user.\",\n curses.color_pair(4) | curses.A_BOLD)\n else:\n self.stdscr.addstr(2, 3, f\"Build FAILED (exit code: {ret})\",\n curses.color_pair(3) | curses.A_BOLD)\n self.stdscr.addstr(5, 3, \"Last output:\", curses.A_BOLD)\n with lock:\n tail = output_lines[-12:] if len(output_lines) > 12 else output_lines\n for i, line in enumerate(tail):\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(6 + i, 3, line[:max_x - 6])\n self.stdscr.addstr(max_y - 1, 0, \" Press Esc to return \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n while self.stdscr.getch() != 27:\n pass\n # #endregion bundle_build_mode\n def refresh_overview(self):\n if not self.candidate_id:\n self.overview = {}\n return\n self.overview = self.facade.get_overview(candidate_id=self.candidate_id)\n def refresh_screen(self):\n max_y, max_x = self.stdscr.getmaxyx()\n self.stdscr.clear()\n try:\n self.draw_header(max_y, max_x)\n self.draw_checks()\n self.draw_sources()\n self.draw_status()\n self.draw_footer(max_y, max_x)\n except Exception:\n pass\n self.stdscr.refresh()\n def loop(self):\n self.refresh_screen()\n while True:\n char = self.stdscr.getch()\n if char == curses.KEY_F10:\n break\n elif char == curses.KEY_F4:\n self.bundle_build_mode()\n self.refresh_screen()\n elif char == curses.KEY_F5:\n self.run_checks()\n elif char == curses.KEY_F6:\n self.build_manifest()\n elif char == curses.KEY_F7:\n self.clear_history()\n elif char == curses.KEY_F8:\n self.approve_latest()\n elif char == curses.KEY_F9:\n self.publish_latest()\n# #endregion CleanReleaseTUI\ndef tui_main(stdscr: curses.window):\n curses.curs_set(0) # Hide cursor\n app = CleanReleaseTUI(stdscr)\n app.loop()\ndef main() -> int:\n seed_trace_id()\n # TUI requires interactive terminal; headless mode must use CLI/API flow.\n if not sys.stdout.isatty():\n print(\n \"TTY is required for TUI mode. Use CLI/API workflow instead.\",\n file=sys.stderr,\n )\n return 2\n try:\n curses.wrapper(tui_main)\n return 0\n except Exception as e:\n print(f\"Error starting TUI: {e}\", file=sys.stderr)\n return 1\nif __name__ == \"__main__\":\n sys.exit(main())\n# #endregion CleanReleaseTuiScript\n" }, { "contract_id": "TuiFacadeAdapter", "contract_type": "Class", "file_path": "backend/src/scripts/clean_release_tui.py", - "start_line": 48, - "end_line": 191, + "start_line": 47, + "end_line": 182, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52672,14 +56535,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TuiFacadeAdapter [TYPE Class]\n# @BRIEF Thin TUI adapter that routes business mutations through application services.\n# @PRE: repository contains candidate and trusted policy/registry snapshots for execution.\n# @POST: Business actions return service results/errors without direct TUI-owned mutations.\nclass TuiFacadeAdapter:\n def __init__(self, repository: CleanReleaseRepository):\n self.repository = repository\n\n def _build_config_manager(self):\n policy = self.repository.get_active_policy()\n if policy is None:\n raise ValueError(\"Active policy not found\")\n clean_release = SimpleNamespace(\n active_policy_id=policy.id,\n active_registry_id=policy.registry_snapshot_id,\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n def run_compliance(self, *, candidate_id: str, actor: str):\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ValueError(\"Manifest required before compliance run\")\n latest_manifest = sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n service = ComplianceExecutionService(\n repository=self.repository,\n config_manager=self._build_config_manager(),\n )\n return service.execute_run(\n candidate_id=candidate_id,\n requested_by=actor,\n manifest_id=latest_manifest.id,\n )\n\n def approve_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for approval\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return approve_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n decided_by=actor,\n comment=\"Approved from TUI\",\n )\n\n def publish_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for publication\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return publish_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n published_by=actor,\n target_channel=\"stable\",\n publication_ref=None,\n )\n\n def build_manifest(self, *, candidate_id: str, actor: str):\n return build_manifest_snapshot(\n repository=self.repository,\n candidate_id=candidate_id,\n created_by=actor,\n )\n\n def get_overview(self, *, candidate_id: str) -> dict[str, Any]:\n candidate = self.repository.get_candidate(candidate_id)\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n runs = [\n item\n for item in self.repository.check_runs.values()\n if item.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(runs, key=lambda item: item.requested_at, reverse=True)[0]\n if runs\n else None\n )\n latest_report = next(\n (\n item\n for item in self.repository.reports.values()\n if latest_run and item.run_id == latest_run.id\n ),\n None,\n )\n approvals = getattr(self.repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approvals if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in approvals)\n else None\n )\n publications = getattr(self.repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publications if item.candidate_id == candidate_id],\n key=lambda item: item.published_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in publications)\n else None\n )\n policy = self.repository.get_active_policy()\n registry = (\n self.repository.get_registry(policy.internal_source_registry_ref)\n if policy\n else None\n )\n return {\n \"candidate\": candidate,\n \"manifest\": latest_manifest,\n \"run\": latest_run,\n \"report\": latest_report,\n \"approval\": latest_approval,\n \"publication\": latest_publication,\n \"policy\": policy,\n \"registry\": registry,\n }\n\n\n# #endregion TuiFacadeAdapter\n" + "body": "# #region TuiFacadeAdapter [TYPE Class]\n# @BRIEF Thin TUI adapter that routes business mutations through application services.\n# @PRE: repository contains candidate and trusted policy/registry snapshots for execution.\n# @POST: Business actions return service results/errors without direct TUI-owned mutations.\nclass TuiFacadeAdapter:\n def __init__(self, repository: CleanReleaseRepository):\n self.repository = repository\n def _build_config_manager(self):\n policy = self.repository.get_active_policy()\n if policy is None:\n raise ValueError(\"Active policy not found\")\n clean_release = SimpleNamespace(\n active_policy_id=policy.id,\n active_registry_id=policy.registry_snapshot_id,\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n def run_compliance(self, *, candidate_id: str, actor: str):\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ValueError(\"Manifest required before compliance run\")\n latest_manifest = sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n service = ComplianceExecutionService(\n repository=self.repository,\n config_manager=self._build_config_manager(),\n )\n return service.execute_run(\n candidate_id=candidate_id,\n requested_by=actor,\n manifest_id=latest_manifest.id,\n )\n def approve_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for approval\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return approve_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n decided_by=actor,\n comment=\"Approved from TUI\",\n )\n def publish_latest(self, *, candidate_id: str, actor: str):\n reports = [\n item\n for item in self.repository.reports.values()\n if item.candidate_id == candidate_id\n ]\n if not reports:\n raise ValueError(\"No compliance report available for publication\")\n report = sorted(reports, key=lambda item: item.generated_at, reverse=True)[0]\n return publish_candidate(\n repository=self.repository,\n candidate_id=candidate_id,\n report_id=report.id,\n published_by=actor,\n target_channel=\"stable\",\n publication_ref=None,\n )\n def build_manifest(self, *, candidate_id: str, actor: str):\n return build_manifest_snapshot(\n repository=self.repository,\n candidate_id=candidate_id,\n created_by=actor,\n )\n def get_overview(self, *, candidate_id: str) -> dict[str, Any]:\n candidate = self.repository.get_candidate(candidate_id)\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda item: item.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n runs = [\n item\n for item in self.repository.check_runs.values()\n if item.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(runs, key=lambda item: item.requested_at, reverse=True)[0]\n if runs\n else None\n )\n latest_report = next(\n (\n item\n for item in self.repository.reports.values()\n if latest_run and item.run_id == latest_run.id\n ),\n None,\n )\n approvals = getattr(self.repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approvals if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in approvals)\n else None\n )\n publications = getattr(self.repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publications if item.candidate_id == candidate_id],\n key=lambda item: item.published_at,\n reverse=True,\n )[0]\n if any(item.candidate_id == candidate_id for item in publications)\n else None\n )\n policy = self.repository.get_active_policy()\n registry = (\n self.repository.get_registry(policy.internal_source_registry_ref)\n if policy\n else None\n )\n return {\n \"candidate\": candidate,\n \"manifest\": latest_manifest,\n \"run\": latest_run,\n \"report\": latest_report,\n \"approval\": latest_approval,\n \"publication\": latest_publication,\n \"policy\": policy,\n \"registry\": registry,\n }\n# #endregion TuiFacadeAdapter\n" }, { "contract_id": "CleanReleaseTUI", "contract_type": "Class", "file_path": "backend/src/scripts/clean_release_tui.py", - "start_line": 194, - "end_line": 656, + "start_line": 183, + "end_line": 758, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52732,14 +56595,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CleanReleaseTUI [TYPE Class]\n# @BRIEF Curses-based application for compliance monitoring.\n# @UX_STATE: READY -> Waiting for operator to start checks (F5).\n# @UX_STATE: RUNNING -> Executing compliance stages with progress feedback.\n# @UX_STATE: COMPLIANT -> Release candidate passed all checks.\n# @UX_STATE: BLOCKED -> Violations detected, release forbidden.\n# @UX_FEEDBACK: Red alerts for BLOCKED status, Green for COMPLIANT.\nclass CleanReleaseTUI:\n def __init__(self, stdscr: curses.window):\n self.stdscr = stdscr\n self.mode = os.getenv(\"CLEAN_TUI_MODE\", \"demo\").strip().lower()\n self.repo = self._build_repository(self.mode)\n self.facade = TuiFacadeAdapter(self.repo)\n self.candidate_id = self._resolve_candidate_id()\n self.status: Any = \"READY\"\n self.checks_progress: list[dict[str, Any]] = []\n self.violations_list: list[ComplianceViolation] = []\n self.report_id: str | None = None\n self.last_error: str | None = None\n self.overview: dict[str, Any] = {}\n self.refresh_overview()\n\n curses.start_color()\n curses.use_default_colors()\n curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header/Footer\n curses.init_pair(2, curses.COLOR_GREEN, -1) # PASS\n curses.init_pair(3, curses.COLOR_RED, -1) # FAIL/BLOCKED\n curses.init_pair(4, curses.COLOR_YELLOW, -1) # RUNNING\n curses.init_pair(5, curses.COLOR_CYAN, -1) # Text\n\n def _build_repository(self, mode: str) -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n if mode == \"demo\":\n self._bootstrap_demo_repository(repo)\n else:\n self._bootstrap_real_repository(repo)\n return repo\n\n def _bootstrap_demo_repository(self, repository: CleanReleaseRepository) -> None:\n now = datetime.now(UTC)\n policy = CleanProfilePolicy(\n policy_id=\"POL-ENT-CLEAN\",\n policy_version=\"1\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"REG-1\",\n prohibited_artifact_categories=[\"test-data\"],\n effective_from=now,\n )\n policy.immutable = True\n repository.save_policy(policy)\n\n registry = ResourceSourceRegistry(\n registry_id=\"REG-1\",\n name=\"Default Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"S1\",\n host=\"internal-repo.company.com\",\n protocol=\"https\",\n purpose=\"artifactory\",\n )\n ],\n updated_at=now,\n updated_by=\"system\",\n )\n registry.immutable = True\n registry.allowed_hosts = [\"internal-repo.company.com\"]\n registry.allowed_schemes = [\"https\"]\n registry.allowed_source_types = [\"artifactory\"]\n repository.save_registry(registry)\n candidate = ReleaseCandidate(\n id=\"2026.03.03-rc1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"v1.0.0-rc1\",\n created_at=now,\n created_by=\"system\",\n status=CandidateStatus.DRAFT.value,\n )\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-1\",\n candidate_id=candidate.id,\n path=\"src/main.py\",\n sha256=\"sha256-demo-core\",\n size=128,\n detected_category=\"core\",\n )\n )\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-2\",\n candidate_id=candidate.id,\n path=\"test/data.csv\",\n sha256=\"sha256-demo-test\",\n size=64,\n detected_category=\"test-data\",\n )\n )\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate.id,\n created_by=\"system\",\n policy_id=\"POL-ENT-CLEAN\",\n )\n summary = dict(manifest.content_json.get(\"summary\", {}))\n summary[\"prohibited_detected_count\"] = 1\n manifest.content_json[\"summary\"] = summary\n\n def _bootstrap_real_repository(self, repository: CleanReleaseRepository) -> None:\n bootstrap_path = os.getenv(\"CLEAN_TUI_BOOTSTRAP_JSON\", \"\").strip()\n if not bootstrap_path:\n return\n\n with open(bootstrap_path, encoding=\"utf-8\") as bootstrap_file:\n payload = json.load(bootstrap_file)\n\n now = datetime.now(UTC)\n candidate = ReleaseCandidate(\n id=payload.get(\"candidate_id\", \"candidate-1\"),\n version=payload.get(\"version\", \"1.0.0\"),\n source_snapshot_ref=payload.get(\"source_snapshot_ref\", \"snapshot-ref\"),\n created_at=now,\n created_by=payload.get(\"created_by\", \"operator\"),\n status=ReleaseCandidateStatus.DRAFT,\n )\n repository.save_candidate(candidate)\n imported_artifacts = load_bootstrap_artifacts(\n os.getenv(\"CLEAN_TUI_ARTIFACTS_JSON\", \"\").strip(),\n candidate.id,\n )\n for artifact in imported_artifacts:\n repository.save_artifact(artifact)\n if imported_artifacts:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n registry_id = payload.get(\"registry_id\", \"REG-1\")\n entries = [\n ResourceSourceEntry(\n source_id=f\"S-{index + 1}\",\n host=host,\n protocol=\"https\",\n purpose=\"bootstrap\",\n enabled=True,\n )\n for index, host in enumerate(payload.get(\"allowed_hosts\", []))\n if str(host).strip()\n ]\n if entries:\n repository.save_registry(\n ResourceSourceRegistry(\n registry_id=registry_id,\n name=payload.get(\"registry_name\", \"Bootstrap Internal Registry\"),\n entries=entries,\n updated_at=now,\n updated_by=payload.get(\"created_by\", \"operator\"),\n status=RegistryStatus.ACTIVE,\n )\n )\n\n if entries:\n repository.save_policy(\n CleanProfilePolicy(\n policy_id=payload.get(\"policy_id\", \"POL-ENT-CLEAN\"),\n policy_version=payload.get(\"policy_version\", \"1\"),\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=registry_id,\n prohibited_artifact_categories=payload.get(\n \"prohibited_artifact_categories\",\n [\"test-data\", \"demo\", \"load-test\"],\n ),\n required_system_categories=payload.get(\n \"required_system_categories\", [\"core\"]\n ),\n effective_from=now,\n )\n )\n\n def _resolve_candidate_id(self) -> str:\n env_candidate = os.getenv(\"CLEAN_TUI_CANDIDATE_ID\", \"\").strip()\n if env_candidate:\n return env_candidate\n\n candidate_ids = list(self.repo.candidates.keys())\n if candidate_ids:\n return candidate_ids[0]\n return \"\"\n\n def draw_header(self, max_y: int, max_x: int):\n header_text = \" Enterprise Clean Release Validator (TUI) \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n # Avoid slicing if possible to satisfy Pyre, or use explicit int\n centered = header_text.center(max_x)\n self.stdscr.addstr(0, 0, centered[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n\n candidate = self.overview.get(\"candidate\")\n candidate_text = self.candidate_id or \"not-set\"\n profile_text = \"enterprise-clean\"\n lifecycle = getattr(candidate, \"status\", \"UNKNOWN\")\n info_line_text = (\n f\" │ Candidate: [{candidate_text}] Profile: [{profile_text}] \"\n f\"Lifecycle: [{lifecycle}] Mode: [{self.mode}]\"\n ).ljust(max_x)\n self.stdscr.addstr(2, 0, info_line_text[:max_x])\n\n def draw_checks(self):\n self.stdscr.addstr(4, 3, \"Checks:\")\n check_defs = [\n (CheckStageName.DATA_PURITY, \"Data Purity (no test/demo payloads)\"),\n (\n CheckStageName.INTERNAL_SOURCES_ONLY,\n \"Internal Sources Only (company servers)\",\n ),\n (CheckStageName.NO_EXTERNAL_ENDPOINTS, \"No External Internet Endpoints\"),\n (CheckStageName.MANIFEST_CONSISTENCY, \"Release Manifest Consistency\"),\n ]\n\n row = 5\n drawn_checks = {c[\"stage\"]: c for c in self.checks_progress}\n\n for stage, desc in check_defs:\n status_text = \" \"\n color = curses.color_pair(5)\n\n if stage in drawn_checks:\n c = drawn_checks[stage]\n if c[\"status\"] == \"RUNNING\":\n status_text = \"...\"\n color = curses.color_pair(4)\n elif c[\"status\"] == CheckStageStatus.PASS:\n status_text = \"PASS\"\n color = curses.color_pair(2)\n elif c[\"status\"] == CheckStageStatus.FAIL:\n status_text = \"FAIL\"\n color = curses.color_pair(3)\n\n self.stdscr.addstr(row, 4, f\"[{status_text:^4}] {desc}\")\n if status_text != \" \":\n self.stdscr.addstr(row, 50, f\"{status_text:>10}\", color | curses.A_BOLD)\n row += 1\n\n def draw_sources(self):\n self.stdscr.addstr(12, 3, \"Allowed Internal Sources:\", curses.A_BOLD)\n reg = self.overview.get(\"registry\")\n row = 13\n if reg:\n for entry in reg.entries:\n self.stdscr.addstr(row, 3, f\" - {entry.host}\")\n row += 1\n else:\n self.stdscr.addstr(row, 3, \" - (none)\")\n\n def draw_status(self):\n color = curses.color_pair(5)\n if self.status == CheckFinalStatus.COMPLIANT:\n color = curses.color_pair(2)\n elif self.status == CheckFinalStatus.BLOCKED:\n color = curses.color_pair(3)\n\n stat_str = str(\n self.status.value if hasattr(self.status, \"value\") else self.status\n )\n self.stdscr.addstr(\n 18, 3, f\"FINAL STATUS: {stat_str.upper()}\", color | curses.A_BOLD\n )\n\n if self.report_id:\n self.stdscr.addstr(19, 3, f\"Report ID: {self.report_id}\")\n\n approval = self.overview.get(\"approval\")\n publication = self.overview.get(\"publication\")\n if approval:\n self.stdscr.addstr(20, 3, f\"Approval: {approval.decision}\")\n if publication:\n self.stdscr.addstr(20, 32, f\"Publication: {publication.status}\")\n\n if self.violations_list:\n self.stdscr.addstr(\n 21,\n 3,\n f\"Violations Details ({len(self.violations_list)} total):\",\n curses.color_pair(3) | curses.A_BOLD,\n )\n row = 22\n for i, v in enumerate(self.violations_list[:5]):\n v_cat = str(getattr(v, \"code\", \"VIOLATION\"))\n msg = str(getattr(v, \"message\", \"Violation detected\"))\n location = str(\n getattr(v, \"artifact_path\", \"\")\n or getattr(getattr(v, \"evidence_json\", {}), \"get\", lambda *_: \"\")(\n \"location\", \"\"\n )\n )\n msg_text = f\"[{v_cat}] {msg} (Loc: {location})\"\n self.stdscr.addstr(row + i, 5, msg_text[:70], curses.color_pair(3))\n if self.last_error:\n self.stdscr.addstr(\n 27,\n 3,\n f\"Error: {self.last_error}\"[:100],\n curses.color_pair(3) | curses.A_BOLD,\n )\n\n def draw_footer(self, max_y: int, max_x: int):\n footer_text = \" F5 Run F6 Manifest F7 Refresh F8 Approve F9 Publish F10 Exit \".center(\n max_x\n )\n self.stdscr.attron(curses.color_pair(1))\n self.stdscr.addstr(max_y - 1, 0, footer_text[:max_x])\n self.stdscr.attroff(curses.color_pair(1))\n\n # [DEF:run_checks:Function]\n # @PURPOSE: Execute compliance run via facade adapter and update UI state.\n # @PRE: Candidate and policy snapshots are present in repository.\n # @POST: UI reflects final run/report/violation state from service result.\n def run_checks(self):\n self.status = \"RUNNING\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_screen()\n\n try:\n result = self.facade.run_compliance(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n except Exception as exc:\n self.status = CheckFinalStatus.FAILED\n self.last_error = str(exc)\n self.refresh_screen()\n return\n\n self.checks_progress = [\n {\n \"stage\": stage.stage_name,\n \"status\": CheckStageStatus.PASS\n if str(stage.decision).upper() == \"PASSED\"\n else CheckStageStatus.FAIL,\n }\n for stage in result.stage_runs\n ]\n self.violations_list = result.violations\n self.report_id = result.report.id if result.report is not None else None\n\n final_status = str(result.run.final_status or \"\").upper()\n if final_status in {\"BLOCKED\", CheckFinalStatus.BLOCKED.value}:\n self.status = CheckFinalStatus.BLOCKED\n elif final_status in {\"COMPLIANT\", \"PASSED\", CheckFinalStatus.COMPLIANT.value}:\n self.status = CheckFinalStatus.COMPLIANT\n else:\n self.status = CheckFinalStatus.FAILED\n self.refresh_overview()\n self.refresh_screen()\n\n # [/DEF:run_checks:Function]\n\n def build_manifest(self):\n try:\n manifest = self.facade.build_manifest(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = f\"Manifest built: {manifest.id}\"\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def clear_history(self):\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_overview()\n self.refresh_screen()\n\n def approve_latest(self):\n if not self.report_id:\n self.last_error = \"F8 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.approve_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def publish_latest(self):\n if not self.report_id:\n self.last_error = \"F9 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.publish_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def refresh_overview(self):\n if not self.report_id:\n self.last_error = \"F9 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.publish_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n\n def refresh_overview(self):\n if not self.candidate_id:\n self.overview = {}\n return\n self.overview = self.facade.get_overview(candidate_id=self.candidate_id)\n\n def refresh_screen(self):\n max_y, max_x = self.stdscr.getmaxyx()\n self.stdscr.clear()\n try:\n self.draw_header(max_y, max_x)\n self.draw_checks()\n self.draw_sources()\n self.draw_status()\n self.draw_footer(max_y, max_x)\n except Exception:\n pass\n self.stdscr.refresh()\n\n def loop(self):\n self.refresh_screen()\n while True:\n char = self.stdscr.getch()\n if char == curses.KEY_F10:\n break\n elif char == curses.KEY_F5:\n self.run_checks()\n elif char == curses.KEY_F6:\n self.build_manifest()\n elif char == curses.KEY_F7:\n self.clear_history()\n elif char == curses.KEY_F8:\n self.approve_latest()\n elif char == curses.KEY_F9:\n self.publish_latest()\n\n\n# #endregion CleanReleaseTUI\n" + "body": "# #region CleanReleaseTUI [TYPE Class]\n# @BRIEF Curses-based application for compliance monitoring.\n# @UX_STATE: READY -> Waiting for operator to start checks (F5).\n# @UX_STATE: RUNNING -> Executing compliance stages with progress feedback.\n# @UX_STATE: COMPLIANT -> Release candidate passed all checks.\n# @UX_STATE: BLOCKED -> Violations detected, release forbidden.\n# @UX_FEEDBACK: Red alerts for BLOCKED status, Green for COMPLIANT.\nclass CleanReleaseTUI:\n def __init__(self, stdscr: curses.window):\n self.stdscr = stdscr\n self.mode = os.getenv(\"CLEAN_TUI_MODE\", \"demo\").strip().lower()\n self.repo = self._build_repository(self.mode)\n self.facade = TuiFacadeAdapter(self.repo)\n self.candidate_id = self._resolve_candidate_id()\n self.status: Any = \"READY\"\n self.checks_progress: list[dict[str, Any]] = []\n self.violations_list: list[ComplianceViolation] = []\n self.report_id: str | None = None\n self.last_error: str | None = None\n self.overview: dict[str, Any] = {}\n self.refresh_overview()\n curses.start_color()\n curses.use_default_colors()\n curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header/Footer\n curses.init_pair(2, curses.COLOR_GREEN, -1) # PASS\n curses.init_pair(3, curses.COLOR_RED, -1) # FAIL/BLOCKED\n curses.init_pair(4, curses.COLOR_YELLOW, -1) # RUNNING\n curses.init_pair(5, curses.COLOR_CYAN, -1) # Text\n def _build_repository(self, mode: str) -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n if mode == \"demo\":\n self._bootstrap_demo_repository(repo)\n else:\n self._bootstrap_real_repository(repo)\n return repo\n def _bootstrap_demo_repository(self, repository: CleanReleaseRepository) -> None:\n now = datetime.now(UTC)\n policy = CleanProfilePolicy(\n policy_id=\"POL-ENT-CLEAN\",\n policy_version=\"1\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"REG-1\",\n prohibited_artifact_categories=[\"test-data\"],\n effective_from=now,\n )\n policy.immutable = True\n repository.save_policy(policy)\n registry = ResourceSourceRegistry(\n registry_id=\"REG-1\",\n name=\"Default Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"S1\",\n host=\"internal-repo.company.com\",\n protocol=\"https\",\n purpose=\"artifactory\",\n )\n ],\n updated_at=now,\n updated_by=\"system\",\n )\n registry.immutable = True\n registry.allowed_hosts = [\"internal-repo.company.com\"]\n registry.allowed_schemes = [\"https\"]\n registry.allowed_source_types = [\"artifactory\"]\n repository.save_registry(registry)\n candidate = ReleaseCandidate(\n id=\"2026.03.03-rc1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"v1.0.0-rc1\",\n created_at=now,\n created_by=\"system\",\n status=CandidateStatus.DRAFT.value,\n )\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-1\",\n candidate_id=candidate.id,\n path=\"src/main.py\",\n sha256=\"sha256-demo-core\",\n size=128,\n detected_category=\"core\",\n )\n )\n repository.save_artifact(\n CandidateArtifact(\n id=\"demo-art-2\",\n candidate_id=candidate.id,\n path=\"test/data.csv\",\n sha256=\"sha256-demo-test\",\n size=64,\n detected_category=\"test-data\",\n )\n )\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate.id,\n created_by=\"system\",\n policy_id=\"POL-ENT-CLEAN\",\n )\n summary = dict(manifest.content_json.get(\"summary\", {}))\n summary[\"prohibited_detected_count\"] = 1\n manifest.content_json[\"summary\"] = summary\n def _bootstrap_real_repository(self, repository: CleanReleaseRepository) -> None:\n bootstrap_path = os.getenv(\"CLEAN_TUI_BOOTSTRAP_JSON\", \"\").strip()\n if not bootstrap_path:\n return\n with open(bootstrap_path, encoding=\"utf-8\") as bootstrap_file:\n payload = json.load(bootstrap_file)\n now = datetime.now(UTC)\n candidate = ReleaseCandidate(\n id=payload.get(\"candidate_id\", \"candidate-1\"),\n version=payload.get(\"version\", \"1.0.0\"),\n source_snapshot_ref=payload.get(\"source_snapshot_ref\", \"snapshot-ref\"),\n created_at=now,\n created_by=payload.get(\"created_by\", \"operator\"),\n status=ReleaseCandidateStatus.DRAFT,\n )\n repository.save_candidate(candidate)\n imported_artifacts = load_bootstrap_artifacts(\n os.getenv(\"CLEAN_TUI_ARTIFACTS_JSON\", \"\").strip(),\n candidate.id,\n )\n for artifact in imported_artifacts:\n repository.save_artifact(artifact)\n if imported_artifacts:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n registry_id = payload.get(\"registry_id\", \"REG-1\")\n entries = [\n ResourceSourceEntry(\n source_id=f\"S-{index + 1}\",\n host=host,\n protocol=\"https\",\n purpose=\"bootstrap\",\n enabled=True,\n )\n for index, host in enumerate(payload.get(\"allowed_hosts\", []))\n if str(host).strip()\n ]\n if entries:\n repository.save_registry(\n ResourceSourceRegistry(\n registry_id=registry_id,\n name=payload.get(\"registry_name\", \"Bootstrap Internal Registry\"),\n entries=entries,\n updated_at=now,\n updated_by=payload.get(\"created_by\", \"operator\"),\n status=RegistryStatus.ACTIVE,\n )\n )\n if entries:\n repository.save_policy(\n CleanProfilePolicy(\n policy_id=payload.get(\"policy_id\", \"POL-ENT-CLEAN\"),\n policy_version=payload.get(\"policy_version\", \"1\"),\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=registry_id,\n prohibited_artifact_categories=payload.get(\n \"prohibited_artifact_categories\",\n [\"test-data\", \"demo\", \"load-test\"],\n ),\n required_system_categories=payload.get(\n \"required_system_categories\", [\"core\"]\n ),\n effective_from=now,\n )\n )\n def _resolve_candidate_id(self) -> str:\n env_candidate = os.getenv(\"CLEAN_TUI_CANDIDATE_ID\", \"\").strip()\n if env_candidate:\n return env_candidate\n candidate_ids = list(self.repo.candidates.keys())\n if candidate_ids:\n return candidate_ids[0]\n return \"\"\n def draw_header(self, max_y: int, max_x: int):\n header_text = \" Enterprise Clean Release Validator (TUI) \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n # Avoid slicing if possible to satisfy Pyre, or use explicit int\n centered = header_text.center(max_x)\n self.stdscr.addstr(0, 0, centered[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n candidate = self.overview.get(\"candidate\")\n candidate_text = self.candidate_id or \"not-set\"\n profile_text = \"enterprise-clean\"\n lifecycle = getattr(candidate, \"status\", \"UNKNOWN\")\n info_line_text = (\n f\" │ Candidate: [{candidate_text}] Profile: [{profile_text}] \"\n f\"Lifecycle: [{lifecycle}] Mode: [{self.mode}]\"\n ).ljust(max_x)\n self.stdscr.addstr(2, 0, info_line_text[:max_x])\n def draw_checks(self):\n self.stdscr.addstr(4, 3, \"Checks:\")\n check_defs = [\n (CheckStageName.DATA_PURITY, \"Data Purity (no test/demo payloads)\"),\n (\n CheckStageName.INTERNAL_SOURCES_ONLY,\n \"Internal Sources Only (company servers)\",\n ),\n (CheckStageName.NO_EXTERNAL_ENDPOINTS, \"No External Internet Endpoints\"),\n (CheckStageName.MANIFEST_CONSISTENCY, \"Release Manifest Consistency\"),\n ]\n row = 5\n drawn_checks = {c[\"stage\"]: c for c in self.checks_progress}\n for stage, desc in check_defs:\n status_text = \" \"\n color = curses.color_pair(5)\n if stage in drawn_checks:\n c = drawn_checks[stage]\n if c[\"status\"] == \"RUNNING\":\n status_text = \"...\"\n color = curses.color_pair(4)\n elif c[\"status\"] == CheckStageStatus.PASS:\n status_text = \"PASS\"\n color = curses.color_pair(2)\n elif c[\"status\"] == CheckStageStatus.FAIL:\n status_text = \"FAIL\"\n color = curses.color_pair(3)\n self.stdscr.addstr(row, 4, f\"[{status_text:^4}] {desc}\")\n if status_text != \" \":\n self.stdscr.addstr(row, 50, f\"{status_text:>10}\", color | curses.A_BOLD)\n row += 1\n def draw_sources(self):\n self.stdscr.addstr(12, 3, \"Allowed Internal Sources:\", curses.A_BOLD)\n reg = self.overview.get(\"registry\")\n row = 13\n if reg:\n for entry in reg.entries:\n self.stdscr.addstr(row, 3, f\" - {entry.host}\")\n row += 1\n else:\n self.stdscr.addstr(row, 3, \" - (none)\")\n def draw_status(self):\n color = curses.color_pair(5)\n if self.status == CheckFinalStatus.COMPLIANT:\n color = curses.color_pair(2)\n elif self.status == CheckFinalStatus.BLOCKED:\n color = curses.color_pair(3)\n stat_str = str(\n self.status.value if hasattr(self.status, \"value\") else self.status\n )\n self.stdscr.addstr(\n 18, 3, f\"FINAL STATUS: {stat_str.upper()}\", color | curses.A_BOLD\n )\n if self.report_id:\n self.stdscr.addstr(19, 3, f\"Report ID: {self.report_id}\")\n approval = self.overview.get(\"approval\")\n publication = self.overview.get(\"publication\")\n if approval:\n self.stdscr.addstr(20, 3, f\"Approval: {approval.decision}\")\n if publication:\n self.stdscr.addstr(20, 32, f\"Publication: {publication.status}\")\n if self.violations_list:\n self.stdscr.addstr(\n 21,\n 3,\n f\"Violations Details ({len(self.violations_list)} total):\",\n curses.color_pair(3) | curses.A_BOLD,\n )\n row = 22\n for i, v in enumerate(self.violations_list[:5]):\n v_cat = str(getattr(v, \"code\", \"VIOLATION\"))\n msg = str(getattr(v, \"message\", \"Violation detected\"))\n location = str(\n getattr(v, \"artifact_path\", \"\")\n or getattr(getattr(v, \"evidence_json\", {}), \"get\", lambda *_: \"\")(\n \"location\", \"\"\n )\n )\n msg_text = f\"[{v_cat}] {msg} (Loc: {location})\"\n self.stdscr.addstr(row + i, 5, msg_text[:70], curses.color_pair(3))\n if self.last_error:\n self.stdscr.addstr(\n 27,\n 3,\n f\"Error: {self.last_error}\"[:100],\n curses.color_pair(3) | curses.A_BOLD,\n )\n def draw_footer(self, max_y: int, max_x: int):\n footer_text = \" F4 Build Bundle F5 Run F6 Manifest F7 Refresh F8 Approve F9 Publish F10 Exit \".center(\n max_x\n )\n self.stdscr.attron(curses.color_pair(1))\n self.stdscr.addstr(max_y - 1, 0, footer_text[:max_x])\n self.stdscr.attroff(curses.color_pair(1))\n # [DEF:run_checks:Function]\n # @PURPOSE: Execute compliance run via facade adapter and update UI state.\n # @PRE: Candidate and policy snapshots are present in repository.\n # @POST: UI reflects final run/report/violation state from service result.\n def run_checks(self):\n self.status = \"RUNNING\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_screen()\n try:\n result = self.facade.run_compliance(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n except Exception as exc:\n self.status = CheckFinalStatus.FAILED\n self.last_error = str(exc)\n self.refresh_screen()\n return\n self.checks_progress = [\n {\n \"stage\": stage.stage_name,\n \"status\": CheckStageStatus.PASS\n if str(stage.decision).upper() == \"PASSED\"\n else CheckStageStatus.FAIL,\n }\n for stage in result.stage_runs\n ]\n self.violations_list = result.violations\n self.report_id = result.report.id if result.report is not None else None\n final_status = str(result.run.final_status or \"\").upper()\n if final_status in {\"BLOCKED\", CheckFinalStatus.BLOCKED.value}:\n self.status = CheckFinalStatus.BLOCKED\n elif final_status in {\"COMPLIANT\", \"PASSED\", CheckFinalStatus.COMPLIANT.value}:\n self.status = CheckFinalStatus.COMPLIANT\n else:\n self.status = CheckFinalStatus.FAILED\n self.refresh_overview()\n self.refresh_screen()\n # [/DEF:run_checks:Function]\n def build_manifest(self):\n try:\n manifest = self.facade.build_manifest(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = f\"Manifest built: {manifest.id}\"\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n def clear_history(self):\n self.status = \"READY\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_overview()\n self.refresh_screen()\n def approve_latest(self):\n if not self.report_id:\n self.last_error = \"F8 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.approve_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n def publish_latest(self):\n if not self.report_id:\n self.last_error = \"F9 disabled: no compliance report available\"\n self.refresh_screen()\n return\n try:\n self.facade.publish_latest(candidate_id=self.candidate_id, actor=\"operator\")\n self.last_error = None\n except Exception as exc:\n self.last_error = str(exc)\n self.refresh_overview()\n self.refresh_screen()\n # #region bundle_build_mode [C:4] [TYPE Function] [SEMANTICS bundle,build,release]\n # @BRIEF Interactive bundle build screen — select type, enter tag, watch live build output.\n # @PRE TTY is available. Bundle type selected (1 or 2). Tag is non-empty.\n # @POST Subprocess completes (success/failure). Output displayed. User returns via Esc.\n # @SIDE_EFFECT Runs docker build subprocess, creates dist/docker/ files\n # @RELATION CALLS -> [EXT:build.sh]\n # @UX_STATE BUNDLE_MENU -> User selects bundle type and enters release tag\n # @UX_STATE BUNDLE_BUILDING -> Subprocess running, real-time output displayed\n # @UX_STATE BUNDLE_DONE -> Build completed with success/failure result\n def bundle_build_mode(self):\n \"\"\"Enter bundle build sub-loop: choose type, enter tag, watch live output.\"\"\"\n max_y, max_x = self.stdscr.getmaxyx()\n # ── Phase 1: Bundle type selection ──\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, \" Bundle Build \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, \"Select bundle type:\", curses.A_BOLD)\n self.stdscr.addstr(5, 5, \"[1] Full Bundle (backend + frontend + postgres — all dependencies)\")\n self.stdscr.addstr(6, 5, \"[2] Light Bundle (~200MB, compressed without Playwright)\")\n self.stdscr.addstr(8, 3, \"Your choice: \")\n self.stdscr.addstr(max_y - 1, 0, \" Esc: Cancel \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n bundle_cmd: str | None = None\n bundle_label: str | None = None\n while True:\n k = self.stdscr.getch()\n if k == ord(\"1\"):\n bundle_cmd, bundle_label = \"bundle\", \"Full Bundle\"\n break\n if k == ord(\"2\"):\n bundle_cmd, bundle_label = \"bundle:light\", \"Light Bundle\"\n break\n if k == 27: # Esc\n return\n # ── Phase 2: Tag input ──\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, f\" Bundle Build — {bundle_label} \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, f\"Type: {bundle_label}\", curses.A_BOLD)\n self.stdscr.addstr(5, 3, \"Release tag (e.g. v1.0.0): \")\n self.stdscr.addstr(max_y - 1, 0, \" Esc: Cancel \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n curses.echo()\n raw_tag = self.stdscr.getstr(5, 33, 30).decode().strip()\n curses.noecho()\n tag = raw_tag.strip()\n if not tag:\n return\n # ── Phase 3: Build execution ──\n build_script = os.path.join(os.path.dirname(BACKEND_ROOT), \"build.sh\")\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, f\" Building {bundle_label} — {tag} \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.refresh()\n try:\n proc = subprocess.Popen(\n [build_script, bundle_cmd, tag],\n stdout=subprocess.PIPE,\n stderr=subprocess.STDOUT,\n text=True,\n bufsize=1,\n )\n except (FileNotFoundError, OSError) as exc:\n self.stdscr.addstr(3, 3, str(exc), curses.color_pair(3))\n self.stdscr.addstr(max_y - 1, 0, \" Press Esc to return \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n while self.stdscr.getch() != 27:\n pass\n return\n output_lines: list[str] = []\n lock = threading.Lock()\n build_done = threading.Event()\n return_code: list[int | None] = [None]\n def _reader_thread() -> None:\n \"\"\"Read subprocess stdout line by line in a background thread.\"\"\"\n try:\n for line in iter(proc.stdout.readline, \"\"):\n with lock:\n output_lines.append(line.rstrip(\"\\n\"))\n proc.stdout.close()\n except ValueError:\n pass\n return_code[0] = proc.wait()\n build_done.set()\n reader = threading.Thread(target=_reader_thread, daemon=True)\n reader.start()\n output_top = 2\n output_height = max_y - 4\n canceled = False\n while not build_done.is_set():\n with lock:\n visible = output_lines[-(output_height - 1):] if output_lines else []\n for i, line in enumerate(visible):\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(output_top + i, 2, line[:max_x - 4])\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(max_y - 2, 2, \" Building... (Esc to cancel) \",\n curses.color_pair(4))\n self.stdscr.refresh()\n self.stdscr.timeout(200)\n k = self.stdscr.getch()\n self.stdscr.timeout(-1)\n if k == 27: # Esc\n proc.terminate()\n canceled = True\n build_done.wait(timeout=5.0)\n if proc.poll() is None:\n proc.kill()\n build_done.wait(timeout=2.0)\n break\n if not canceled:\n build_done.wait()\n try:\n remaining = proc.stdout.read()\n if remaining:\n with lock:\n for line in remaining.rstrip(\"\\n\").split(\"\\n\"):\n if line:\n output_lines.append(line)\n except Exception:\n pass\n # ── Phase 4: Result summary ──\n self.stdscr.clear()\n ret = return_code[0]\n if ret == 0:\n title = f\" ✅ BUILD COMPLETE — {bundle_label} / {tag} \"\n elif canceled:\n title = f\" ⏹ BUILD CANCELED — {bundle_label} / {tag} \"\n else:\n title = f\" ❌ BUILD FAILED — {bundle_label} / {tag} \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, title.center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n if ret == 0:\n self.stdscr.addstr(2, 3, \"Build completed successfully!\",\n curses.color_pair(2) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, \"Output: dist/docker/\",\n curses.color_pair(2))\n elif canceled:\n self.stdscr.addstr(2, 3, \"Build canceled by user.\",\n curses.color_pair(4) | curses.A_BOLD)\n else:\n self.stdscr.addstr(2, 3, f\"Build FAILED (exit code: {ret})\",\n curses.color_pair(3) | curses.A_BOLD)\n self.stdscr.addstr(5, 3, \"Last output:\", curses.A_BOLD)\n with lock:\n tail = output_lines[-12:] if len(output_lines) > 12 else output_lines\n for i, line in enumerate(tail):\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(6 + i, 3, line[:max_x - 6])\n self.stdscr.addstr(max_y - 1, 0, \" Press Esc to return \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n while self.stdscr.getch() != 27:\n pass\n # #endregion bundle_build_mode\n def refresh_overview(self):\n if not self.candidate_id:\n self.overview = {}\n return\n self.overview = self.facade.get_overview(candidate_id=self.candidate_id)\n def refresh_screen(self):\n max_y, max_x = self.stdscr.getmaxyx()\n self.stdscr.clear()\n try:\n self.draw_header(max_y, max_x)\n self.draw_checks()\n self.draw_sources()\n self.draw_status()\n self.draw_footer(max_y, max_x)\n except Exception:\n pass\n self.stdscr.refresh()\n def loop(self):\n self.refresh_screen()\n while True:\n char = self.stdscr.getch()\n if char == curses.KEY_F10:\n break\n elif char == curses.KEY_F4:\n self.bundle_build_mode()\n self.refresh_screen()\n elif char == curses.KEY_F5:\n self.run_checks()\n elif char == curses.KEY_F6:\n self.build_manifest()\n elif char == curses.KEY_F7:\n self.clear_history()\n elif char == curses.KEY_F8:\n self.approve_latest()\n elif char == curses.KEY_F9:\n self.publish_latest()\n# #endregion CleanReleaseTUI\n" }, { "contract_id": "run_checks", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_tui.py", - "start_line": 510, - "end_line": 554, + "start_line": 473, + "end_line": 513, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52780,21 +56643,73 @@ ], "anchor_syntax": "def", "tier_source": "AutoCalculated", - "body": " # [DEF:run_checks:Function]\n # @PURPOSE: Execute compliance run via facade adapter and update UI state.\n # @PRE: Candidate and policy snapshots are present in repository.\n # @POST: UI reflects final run/report/violation state from service result.\n def run_checks(self):\n self.status = \"RUNNING\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_screen()\n\n try:\n result = self.facade.run_compliance(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n except Exception as exc:\n self.status = CheckFinalStatus.FAILED\n self.last_error = str(exc)\n self.refresh_screen()\n return\n\n self.checks_progress = [\n {\n \"stage\": stage.stage_name,\n \"status\": CheckStageStatus.PASS\n if str(stage.decision).upper() == \"PASSED\"\n else CheckStageStatus.FAIL,\n }\n for stage in result.stage_runs\n ]\n self.violations_list = result.violations\n self.report_id = result.report.id if result.report is not None else None\n\n final_status = str(result.run.final_status or \"\").upper()\n if final_status in {\"BLOCKED\", CheckFinalStatus.BLOCKED.value}:\n self.status = CheckFinalStatus.BLOCKED\n elif final_status in {\"COMPLIANT\", \"PASSED\", CheckFinalStatus.COMPLIANT.value}:\n self.status = CheckFinalStatus.COMPLIANT\n else:\n self.status = CheckFinalStatus.FAILED\n self.refresh_overview()\n self.refresh_screen()\n\n # [/DEF:run_checks:Function]\n" + "body": " # [DEF:run_checks:Function]\n # @PURPOSE: Execute compliance run via facade adapter and update UI state.\n # @PRE: Candidate and policy snapshots are present in repository.\n # @POST: UI reflects final run/report/violation state from service result.\n def run_checks(self):\n self.status = \"RUNNING\"\n self.report_id = None\n self.violations_list = []\n self.checks_progress = []\n self.last_error = None\n self.refresh_screen()\n try:\n result = self.facade.run_compliance(\n candidate_id=self.candidate_id, actor=\"operator\"\n )\n except Exception as exc:\n self.status = CheckFinalStatus.FAILED\n self.last_error = str(exc)\n self.refresh_screen()\n return\n self.checks_progress = [\n {\n \"stage\": stage.stage_name,\n \"status\": CheckStageStatus.PASS\n if str(stage.decision).upper() == \"PASSED\"\n else CheckStageStatus.FAIL,\n }\n for stage in result.stage_runs\n ]\n self.violations_list = result.violations\n self.report_id = result.report.id if result.report is not None else None\n final_status = str(result.run.final_status or \"\").upper()\n if final_status in {\"BLOCKED\", CheckFinalStatus.BLOCKED.value}:\n self.status = CheckFinalStatus.BLOCKED\n elif final_status in {\"COMPLIANT\", \"PASSED\", CheckFinalStatus.COMPLIANT.value}:\n self.status = CheckFinalStatus.COMPLIANT\n else:\n self.status = CheckFinalStatus.FAILED\n self.refresh_overview()\n self.refresh_screen()\n # [/DEF:run_checks:Function]\n" + }, + { + "contract_id": "bundle_build_mode", + "contract_type": "Function", + "file_path": "backend/src/scripts/clean_release_tui.py", + "start_line": 560, + "end_line": 721, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Subprocess completes (success/failure). Output displayed. User returns via Esc.", + "PRE": "TTY is available. Bundle type selected (1 or 2). Tag is non-empty.", + "PURPOSE": "Interactive bundle build screen — select type, enter tag, watch live build output.", + "SIDE_EFFECT": "Runs docker build subprocess, creates dist/docker/ files", + "UX_STATE": "BUNDLE_DONE -> Build completed with success/failure result" + }, + "relations": [ + { + "source_id": "bundle_build_mode", + "relation_type": "CALLS", + "target_id": "EXT:build.sh", + "target_ref": "[EXT:build.sh]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Function' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region bundle_build_mode [C:4] [TYPE Function] [SEMANTICS bundle,build,release]\n # @BRIEF Interactive bundle build screen — select type, enter tag, watch live build output.\n # @PRE TTY is available. Bundle type selected (1 or 2). Tag is non-empty.\n # @POST Subprocess completes (success/failure). Output displayed. User returns via Esc.\n # @SIDE_EFFECT Runs docker build subprocess, creates dist/docker/ files\n # @RELATION CALLS -> [EXT:build.sh]\n # @UX_STATE BUNDLE_MENU -> User selects bundle type and enters release tag\n # @UX_STATE BUNDLE_BUILDING -> Subprocess running, real-time output displayed\n # @UX_STATE BUNDLE_DONE -> Build completed with success/failure result\n def bundle_build_mode(self):\n \"\"\"Enter bundle build sub-loop: choose type, enter tag, watch live output.\"\"\"\n max_y, max_x = self.stdscr.getmaxyx()\n # ── Phase 1: Bundle type selection ──\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, \" Bundle Build \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, \"Select bundle type:\", curses.A_BOLD)\n self.stdscr.addstr(5, 5, \"[1] Full Bundle (backend + frontend + postgres — all dependencies)\")\n self.stdscr.addstr(6, 5, \"[2] Light Bundle (~200MB, compressed without Playwright)\")\n self.stdscr.addstr(8, 3, \"Your choice: \")\n self.stdscr.addstr(max_y - 1, 0, \" Esc: Cancel \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n bundle_cmd: str | None = None\n bundle_label: str | None = None\n while True:\n k = self.stdscr.getch()\n if k == ord(\"1\"):\n bundle_cmd, bundle_label = \"bundle\", \"Full Bundle\"\n break\n if k == ord(\"2\"):\n bundle_cmd, bundle_label = \"bundle:light\", \"Light Bundle\"\n break\n if k == 27: # Esc\n return\n # ── Phase 2: Tag input ──\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, f\" Bundle Build — {bundle_label} \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, f\"Type: {bundle_label}\", curses.A_BOLD)\n self.stdscr.addstr(5, 3, \"Release tag (e.g. v1.0.0): \")\n self.stdscr.addstr(max_y - 1, 0, \" Esc: Cancel \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n curses.echo()\n raw_tag = self.stdscr.getstr(5, 33, 30).decode().strip()\n curses.noecho()\n tag = raw_tag.strip()\n if not tag:\n return\n # ── Phase 3: Build execution ──\n build_script = os.path.join(os.path.dirname(BACKEND_ROOT), \"build.sh\")\n self.stdscr.clear()\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, f\" Building {bundle_label} — {tag} \".center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.refresh()\n try:\n proc = subprocess.Popen(\n [build_script, bundle_cmd, tag],\n stdout=subprocess.PIPE,\n stderr=subprocess.STDOUT,\n text=True,\n bufsize=1,\n )\n except (FileNotFoundError, OSError) as exc:\n self.stdscr.addstr(3, 3, str(exc), curses.color_pair(3))\n self.stdscr.addstr(max_y - 1, 0, \" Press Esc to return \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n while self.stdscr.getch() != 27:\n pass\n return\n output_lines: list[str] = []\n lock = threading.Lock()\n build_done = threading.Event()\n return_code: list[int | None] = [None]\n def _reader_thread() -> None:\n \"\"\"Read subprocess stdout line by line in a background thread.\"\"\"\n try:\n for line in iter(proc.stdout.readline, \"\"):\n with lock:\n output_lines.append(line.rstrip(\"\\n\"))\n proc.stdout.close()\n except ValueError:\n pass\n return_code[0] = proc.wait()\n build_done.set()\n reader = threading.Thread(target=_reader_thread, daemon=True)\n reader.start()\n output_top = 2\n output_height = max_y - 4\n canceled = False\n while not build_done.is_set():\n with lock:\n visible = output_lines[-(output_height - 1):] if output_lines else []\n for i, line in enumerate(visible):\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(output_top + i, 2, line[:max_x - 4])\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(max_y - 2, 2, \" Building... (Esc to cancel) \",\n curses.color_pair(4))\n self.stdscr.refresh()\n self.stdscr.timeout(200)\n k = self.stdscr.getch()\n self.stdscr.timeout(-1)\n if k == 27: # Esc\n proc.terminate()\n canceled = True\n build_done.wait(timeout=5.0)\n if proc.poll() is None:\n proc.kill()\n build_done.wait(timeout=2.0)\n break\n if not canceled:\n build_done.wait()\n try:\n remaining = proc.stdout.read()\n if remaining:\n with lock:\n for line in remaining.rstrip(\"\\n\").split(\"\\n\"):\n if line:\n output_lines.append(line)\n except Exception:\n pass\n # ── Phase 4: Result summary ──\n self.stdscr.clear()\n ret = return_code[0]\n if ret == 0:\n title = f\" ✅ BUILD COMPLETE — {bundle_label} / {tag} \"\n elif canceled:\n title = f\" ⏹ BUILD CANCELED — {bundle_label} / {tag} \"\n else:\n title = f\" ❌ BUILD FAILED — {bundle_label} / {tag} \"\n self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD)\n self.stdscr.addstr(0, 0, title.center(max_x)[:max_x])\n self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)\n if ret == 0:\n self.stdscr.addstr(2, 3, \"Build completed successfully!\",\n curses.color_pair(2) | curses.A_BOLD)\n self.stdscr.addstr(3, 3, \"Output: dist/docker/\",\n curses.color_pair(2))\n elif canceled:\n self.stdscr.addstr(2, 3, \"Build canceled by user.\",\n curses.color_pair(4) | curses.A_BOLD)\n else:\n self.stdscr.addstr(2, 3, f\"Build FAILED (exit code: {ret})\",\n curses.color_pair(3) | curses.A_BOLD)\n self.stdscr.addstr(5, 3, \"Last output:\", curses.A_BOLD)\n with lock:\n tail = output_lines[-12:] if len(output_lines) > 12 else output_lines\n for i, line in enumerate(tail):\n with contextlib.suppress(curses.error):\n self.stdscr.addstr(6 + i, 3, line[:max_x - 6])\n self.stdscr.addstr(max_y - 1, 0, \" Press Esc to return \".center(max_x)[:max_x],\n curses.color_pair(1))\n self.stdscr.refresh()\n while self.stdscr.getch() != 27:\n pass\n # #endregion bundle_build_mode\n" }, { "contract_id": "CreateAdminScript", "contract_type": "Module", "file_path": "backend/src/scripts/create_admin.py", "start_line": 1, - "end_line": 94, - "tier": "TIER_2", - "complexity": 3, + "end_line": 96, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "CLIArgs -> AdminUser", "INVARIANT": "Admin user must have the \"Admin\" role.", - "LAYER": "Scripts", - "PURPOSE": "CLI tool for creating the initial admin user." + "LAYER": "Infrastructure", + "PURPOSE": "CLI tool for creating the initial admin user.", + "SIDE_EFFECT": "Writes admin user to database" }, "relations": [ { @@ -52818,29 +56733,21 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, "contract_type": "Module" } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Scripts" + "actual_complexity": 5, + "contract_type": "Module" } }, { @@ -52900,14 +56807,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CreateAdminScript [C:3] [TYPE Module] [SEMANTICS admin, search, user, cli]\n#\n# @BRIEF CLI tool for creating the initial admin user.\n# @LAYER: Scripts\n# @RELATION USES -> [AuthSecurityModule]\n# @RELATION USES -> [DatabaseModule]\n# @RELATION USES -> [AuthModels]\n#\n# @INVARIANT: Admin user must have the \"Admin\" role.\n\nimport argparse\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.auth.security import get_password_hash\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.database import AuthSessionLocal, init_db\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import Role, User\n\n\n# #region create_admin [TYPE Function]\n# @BRIEF Creates an admin user and necessary roles/permissions.\n# @PRE: username and password provided via CLI.\n# @POST: Admin user exists in auth.db.\n#\ndef create_admin(username, password, email=None):\n seed_trace_id()\n with belief_scope(\"create_admin\"):\n db = AuthSessionLocal()\n try:\n normalized_email = (\n email.strip() if isinstance(email, str) and email.strip() else None\n )\n\n # 1. Ensure Admin role exists\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n logger.info(\"Creating Admin role...\")\n admin_role = Role(name=\"Admin\", description=\"System Administrator\")\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n\n # 2. Check if user already exists\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.warning(f\"User {username} already exists.\")\n return \"exists\"\n\n # 3. Create Admin user\n logger.info(f\"Creating admin user: {username}\")\n new_user = User(\n username=username,\n email=normalized_email,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(f\"Admin user {username} created successfully.\")\n return \"created\"\n\n except Exception as e:\n logger.error(f\"Failed to create admin user: {e}\")\n db.rollback()\n raise\n finally:\n db.close()\n\n\n# #endregion create_admin\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(description=\"Create initial admin user\")\n parser.add_argument(\"--username\", required=True, help=\"Admin username\")\n parser.add_argument(\"--password\", required=True, help=\"Admin password\")\n parser.add_argument(\"--email\", required=False, help=\"Admin email\")\n args = parser.parse_args()\n\n try:\n # Ensure DB is initialized before creating admin\n init_db()\n create_admin(args.username, args.password, args.email)\n sys.exit(0)\n except Exception:\n sys.exit(1)\n\n# #endregion CreateAdminScript\n" + "body": "# #region CreateAdminScript [C:5] [TYPE Module] [SEMANTICS admin, search, user, cli]\n#\n# @BRIEF CLI tool for creating the initial admin user.\n# @LAYER: Infrastructure\n# @RELATION USES -> [AuthSecurityModule]\n# @RELATION USES -> [DatabaseModule]\n# @RELATION USES -> [AuthModels]\n#\n# @INVARIANT: Admin user must have the \"Admin\" role.\n# @SIDE_EFFECT: Writes admin user to database\n# @DATA_CONTRACT: CLIArgs -> AdminUser\n\nimport argparse\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.auth.security import get_password_hash\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.database import AuthSessionLocal, init_db\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import Role, User\n\n\n# #region create_admin [TYPE Function]\n# @BRIEF Creates an admin user and necessary roles/permissions.\n# @PRE: username and password provided via CLI.\n# @POST: Admin user exists in auth.db.\n#\ndef create_admin(username, password, email=None):\n seed_trace_id()\n with belief_scope(\"create_admin\"):\n db = AuthSessionLocal()\n try:\n normalized_email = (\n email.strip() if isinstance(email, str) and email.strip() else None\n )\n\n # 1. Ensure Admin role exists\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n logger.info(\"Creating Admin role...\")\n admin_role = Role(name=\"Admin\", description=\"System Administrator\")\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n\n # 2. Check if user already exists\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.warning(f\"User {username} already exists.\")\n return \"exists\"\n\n # 3. Create Admin user\n logger.info(f\"Creating admin user: {username}\")\n new_user = User(\n username=username,\n email=normalized_email,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(f\"Admin user {username} created successfully.\")\n return \"created\"\n\n except Exception as e:\n logger.error(f\"Failed to create admin user: {e}\")\n db.rollback()\n raise\n finally:\n db.close()\n\n\n# #endregion create_admin\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(description=\"Create initial admin user\")\n parser.add_argument(\"--username\", required=True, help=\"Admin username\")\n parser.add_argument(\"--password\", required=True, help=\"Admin password\")\n parser.add_argument(\"--email\", required=False, help=\"Admin email\")\n args = parser.parse_args()\n\n try:\n # Ensure DB is initialized before creating admin\n init_db()\n create_admin(args.username, args.password, args.email)\n sys.exit(0)\n except Exception:\n sys.exit(1)\n\n# #endregion CreateAdminScript\n" }, { "contract_id": "create_admin", "contract_type": "Function", "file_path": "backend/src/scripts/create_admin.py", - "start_line": 25, - "end_line": 77, + "start_line": 27, + "end_line": 79, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53068,14 +56975,16 @@ "contract_type": "Module", "file_path": "backend/src/scripts/seed_permissions.py", "start_line": 1, - "end_line": 144, - "tier": "TIER_2", - "complexity": 3, + "end_line": 146, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "CLIArgs -> SeedSummary", "INVARIANT": "Safe to run multiple times (idempotent).", - "LAYER": "Scripts", - "PURPOSE": "Populates the auth database with initial system permissions." + "LAYER": "Infrastructure", + "PURPOSE": "Populates the auth database with initial system permissions.", + "SIDE_EFFECT": "Writes permissions to database" }, "relations": [ { @@ -53105,42 +57014,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, "contract_type": "Module" } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Scripts" + "actual_complexity": 5, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SeedPermissionsScript [C:3] [TYPE Module] [SEMANTICS rbac, search, auth]\n#\n# @BRIEF Populates the auth database with initial system permissions.\n# @LAYER: Scripts\n# @RELATION DEPENDS_ON -> AuthSessionLocal\n# @RELATION DEPENDS_ON -> Permission\n# @RELATION DEPENDS_ON -> Role\n# @RELATION DEPENDS_ON -> AuthRepository\n#\n# @INVARIANT: Safe to run multiple times (idempotent).\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.database import AuthSessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import Permission, Role\n\n# #region INITIAL_PERMISSIONS [C:3] [TYPE Constant]\n# @BRIEF Canonical bootstrap permission tuples seeded into auth storage.\n# @RELATION DEPENDS_ON -> SeedPermissionsScript\nINITIAL_PERMISSIONS = [\n # Admin Permissions\n {\"resource\": \"admin:users\", \"action\": \"READ\"},\n {\"resource\": \"admin:users\", \"action\": \"WRITE\"},\n {\"resource\": \"admin:roles\", \"action\": \"READ\"},\n {\"resource\": \"admin:roles\", \"action\": \"WRITE\"},\n {\"resource\": \"admin:settings\", \"action\": \"READ\"},\n {\"resource\": \"admin:settings\", \"action\": \"WRITE\"},\n {\"resource\": \"environments\", \"action\": \"READ\"},\n {\"resource\": \"plugins\", \"action\": \"READ\"},\n {\"resource\": \"tasks\", \"action\": \"READ\"},\n {\"resource\": \"tasks\", \"action\": \"WRITE\"},\n # Plugin Permissions\n {\"resource\": \"plugin:backup\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:migration\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:mapper\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:search\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:git\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:storage\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:storage\", \"action\": \"READ\"},\n {\"resource\": \"plugin:storage\", \"action\": \"WRITE\"},\n {\"resource\": \"plugin:debug\", \"action\": \"EXECUTE\"},\n {\"resource\": \"git_config\", \"action\": \"READ\"},\n # Dataset Review Permissions\n {\"resource\": \"dataset:session\", \"action\": \"READ\"},\n {\"resource\": \"dataset:session\", \"action\": \"MANAGE\"},\n {\"resource\": \"dataset:session\", \"action\": \"APPROVE\"},\n {\"resource\": \"dataset:execution\", \"action\": \"PREVIEW\"},\n {\"resource\": \"dataset:execution\", \"action\": \"LAUNCH\"},\n {\"resource\": \"dataset:execution\", \"action\": \"LAUNCH_PROD\"},\n]\n# #endregion INITIAL_PERMISSIONS\n\n\n# #region seed_permissions [C:3] [TYPE Function]\n# @BRIEF Inserts missing permissions into the database.\n# @POST: All INITIAL_PERMISSIONS exist in the DB.\n# @RELATION DEPENDS_ON -> AuthSessionLocal\n# @RELATION DEPENDS_ON -> Permission\n# @RELATION DEPENDS_ON -> Role\n# @RELATION DEPENDS_ON -> AuthRepository\n# @RELATION DEPENDS_ON -> INITIAL_PERMISSIONS\ndef seed_permissions():\n seed_trace_id()\n with belief_scope(\"seed_permissions\"):\n db = AuthSessionLocal()\n try:\n logger.info(\"Seeding permissions...\")\n count = 0\n for perm_data in INITIAL_PERMISSIONS:\n exists = (\n db.query(Permission)\n .filter(\n Permission.resource == perm_data[\"resource\"],\n Permission.action == perm_data[\"action\"],\n )\n .first()\n )\n\n if not exists:\n new_perm = Permission(\n resource=perm_data[\"resource\"], action=perm_data[\"action\"]\n )\n db.add(new_perm)\n count += 1\n\n db.commit()\n logger.info(f\"Seeding completed. Added {count} new permissions.\")\n\n # Assign permissions to User role\n repo = AuthRepository(db)\n user_role = repo.get_role_by_name(\"User\")\n if not user_role:\n user_role = Role(\n name=\"User\", description=\"Standard user with plugin access\"\n )\n db.add(user_role)\n db.flush()\n\n user_permissions = [\n (\"plugin:mapper\", \"EXECUTE\"),\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:backup\", \"EXECUTE\"),\n (\"plugin:git\", \"EXECUTE\"),\n (\"plugin:storage\", \"READ\"),\n (\"plugin:storage\", \"WRITE\"),\n (\"environments\", \"READ\"),\n (\"plugins\", \"READ\"),\n (\"tasks\", \"READ\"),\n (\"tasks\", \"WRITE\"),\n (\"git_config\", \"READ\"),\n (\"dataset:session\", \"READ\"),\n (\"dataset:session\", \"MANAGE\"),\n (\"dataset:execution\", \"PREVIEW\"),\n (\"dataset:execution\", \"LAUNCH\"),\n ]\n\n for res, act in user_permissions:\n perm = repo.get_permission_by_resource_action(res, act)\n if perm and perm not in user_role.permissions:\n user_role.permissions.append(perm)\n\n db.commit()\n logger.info(\"User role permissions updated.\")\n\n except Exception as e:\n logger.error(f\"Failed to seed permissions: {e}\")\n db.rollback()\n finally:\n db.close()\n\n\n# #endregion seed_permissions\n\nif __name__ == \"__main__\":\n seed_permissions()\n\n# #endregion SeedPermissionsScript\n" + "body": "# #region SeedPermissionsScript [C:5] [TYPE Module] [SEMANTICS rbac, search, auth]\n#\n# @BRIEF Populates the auth database with initial system permissions.\n# @LAYER: Infrastructure\n# @RELATION DEPENDS_ON -> AuthSessionLocal\n# @RELATION DEPENDS_ON -> Permission\n# @RELATION DEPENDS_ON -> Role\n# @RELATION DEPENDS_ON -> AuthRepository\n#\n# @INVARIANT: Safe to run multiple times (idempotent).\n# @SIDE_EFFECT: Writes permissions to database\n# @DATA_CONTRACT: CLIArgs -> SeedSummary\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.database import AuthSessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import Permission, Role\n\n# #region INITIAL_PERMISSIONS [C:3] [TYPE Constant]\n# @BRIEF Canonical bootstrap permission tuples seeded into auth storage.\n# @RELATION DEPENDS_ON -> SeedPermissionsScript\nINITIAL_PERMISSIONS = [\n # Admin Permissions\n {\"resource\": \"admin:users\", \"action\": \"READ\"},\n {\"resource\": \"admin:users\", \"action\": \"WRITE\"},\n {\"resource\": \"admin:roles\", \"action\": \"READ\"},\n {\"resource\": \"admin:roles\", \"action\": \"WRITE\"},\n {\"resource\": \"admin:settings\", \"action\": \"READ\"},\n {\"resource\": \"admin:settings\", \"action\": \"WRITE\"},\n {\"resource\": \"environments\", \"action\": \"READ\"},\n {\"resource\": \"plugins\", \"action\": \"READ\"},\n {\"resource\": \"tasks\", \"action\": \"READ\"},\n {\"resource\": \"tasks\", \"action\": \"WRITE\"},\n # Plugin Permissions\n {\"resource\": \"plugin:backup\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:migration\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:mapper\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:search\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:git\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:storage\", \"action\": \"EXECUTE\"},\n {\"resource\": \"plugin:storage\", \"action\": \"READ\"},\n {\"resource\": \"plugin:storage\", \"action\": \"WRITE\"},\n {\"resource\": \"plugin:debug\", \"action\": \"EXECUTE\"},\n {\"resource\": \"git_config\", \"action\": \"READ\"},\n # Dataset Review Permissions\n {\"resource\": \"dataset:session\", \"action\": \"READ\"},\n {\"resource\": \"dataset:session\", \"action\": \"MANAGE\"},\n {\"resource\": \"dataset:session\", \"action\": \"APPROVE\"},\n {\"resource\": \"dataset:execution\", \"action\": \"PREVIEW\"},\n {\"resource\": \"dataset:execution\", \"action\": \"LAUNCH\"},\n {\"resource\": \"dataset:execution\", \"action\": \"LAUNCH_PROD\"},\n]\n# #endregion INITIAL_PERMISSIONS\n\n\n# #region seed_permissions [C:3] [TYPE Function]\n# @BRIEF Inserts missing permissions into the database.\n# @POST: All INITIAL_PERMISSIONS exist in the DB.\n# @RELATION DEPENDS_ON -> AuthSessionLocal\n# @RELATION DEPENDS_ON -> Permission\n# @RELATION DEPENDS_ON -> Role\n# @RELATION DEPENDS_ON -> AuthRepository\n# @RELATION DEPENDS_ON -> INITIAL_PERMISSIONS\ndef seed_permissions():\n seed_trace_id()\n with belief_scope(\"seed_permissions\"):\n db = AuthSessionLocal()\n try:\n logger.info(\"Seeding permissions...\")\n count = 0\n for perm_data in INITIAL_PERMISSIONS:\n exists = (\n db.query(Permission)\n .filter(\n Permission.resource == perm_data[\"resource\"],\n Permission.action == perm_data[\"action\"],\n )\n .first()\n )\n\n if not exists:\n new_perm = Permission(\n resource=perm_data[\"resource\"], action=perm_data[\"action\"]\n )\n db.add(new_perm)\n count += 1\n\n db.commit()\n logger.info(f\"Seeding completed. Added {count} new permissions.\")\n\n # Assign permissions to User role\n repo = AuthRepository(db)\n user_role = repo.get_role_by_name(\"User\")\n if not user_role:\n user_role = Role(\n name=\"User\", description=\"Standard user with plugin access\"\n )\n db.add(user_role)\n db.flush()\n\n user_permissions = [\n (\"plugin:mapper\", \"EXECUTE\"),\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:backup\", \"EXECUTE\"),\n (\"plugin:git\", \"EXECUTE\"),\n (\"plugin:storage\", \"READ\"),\n (\"plugin:storage\", \"WRITE\"),\n (\"environments\", \"READ\"),\n (\"plugins\", \"READ\"),\n (\"tasks\", \"READ\"),\n (\"tasks\", \"WRITE\"),\n (\"git_config\", \"READ\"),\n (\"dataset:session\", \"READ\"),\n (\"dataset:session\", \"MANAGE\"),\n (\"dataset:execution\", \"PREVIEW\"),\n (\"dataset:execution\", \"LAUNCH\"),\n ]\n\n for res, act in user_permissions:\n perm = repo.get_permission_by_resource_action(res, act)\n if perm and perm not in user_role.permissions:\n user_role.permissions.append(perm)\n\n db.commit()\n logger.info(\"User role permissions updated.\")\n\n except Exception as e:\n logger.error(f\"Failed to seed permissions: {e}\")\n db.rollback()\n finally:\n db.close()\n\n\n# #endregion seed_permissions\n\nif __name__ == \"__main__\":\n seed_permissions()\n\n# #endregion SeedPermissionsScript\n" }, { "contract_id": "INITIAL_PERMISSIONS", "contract_type": "Constant", "file_path": "backend/src/scripts/seed_permissions.py", - "start_line": 24, - "end_line": 58, + "start_line": 26, + "end_line": 60, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -53227,8 +57128,8 @@ "contract_id": "seed_permissions", "contract_type": "Function", "file_path": "backend/src/scripts/seed_permissions.py", - "start_line": 61, - "end_line": 139, + "start_line": 63, + "end_line": 141, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -53288,13 +57189,14 @@ "contract_type": "Module", "file_path": "backend/src/scripts/seed_superset_load_test.py", "start_line": 1, - "end_line": 401, - "tier": "TIER_2", - "complexity": 3, + "end_line": 402, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "CLIArgs -> TestDataSummary", "INVARIANT": "Created chart and dashboard names are globally unique for one script run.", - "LAYER": "Scripts", + "LAYER": "Infrastructure", "PURPOSE": "Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments." }, "relations": [ @@ -53313,29 +57215,30 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, "contract_type": "Module" } }, { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Scripts' is not in allowed enum", + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Scripts" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } }, { @@ -53377,14 +57280,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SeedSupersetLoadTestScript [C:3] [TYPE Module] [SEMANTICS superset, validate]\n#\n# @BRIEF Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments.\n# @LAYER: Scripts\n# @RELATION USES -> [ConfigManager]\n# @RELATION USES -> [SupersetClient]\n# @INVARIANT: Created chart and dashboard names are globally unique for one script run.\n\nimport argparse\nimport json\nimport random\nimport sys\nimport uuid\nfrom pathlib import Path\n\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.config_models import Environment\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\n\n\n# #region _parse_args [TYPE Function]\n# @BRIEF Parses CLI arguments for load-test data generation.\n# @PRE: Script is called from CLI.\n# @POST: Returns validated argument namespace.\ndef _parse_args() -> argparse.Namespace:\n parser = argparse.ArgumentParser(\n description=\"Seed Superset with load-test charts and dashboards\"\n )\n parser.add_argument(\n \"--envs\", nargs=\"+\", default=[\"ss1\", \"ss2\"], help=\"Target environment IDs\"\n )\n parser.add_argument(\n \"--charts\", type=int, default=10000, help=\"Target number of charts to create\"\n )\n parser.add_argument(\n \"--dashboards\",\n type=int,\n default=500,\n help=\"Target number of dashboards to create\",\n )\n parser.add_argument(\n \"--template-pool-size\",\n type=int,\n default=200,\n help=\"How many source charts to sample as templates per env\",\n )\n parser.add_argument(\n \"--seed\", type=int, default=None, help=\"Optional RNG seed for reproducibility\"\n )\n parser.add_argument(\n \"--max-errors\",\n type=int,\n default=100,\n help=\"Stop early if errors exceed this threshold\",\n )\n parser.add_argument(\n \"--dry-run\", action=\"store_true\", help=\"Do not write data, only validate setup\"\n )\n return parser.parse_args()\n\n\n# #endregion _parse_args\n\n\n# #region _extract_result_payload [TYPE Function]\n# @BRIEF Normalizes Superset API payloads that may be wrapped in `result`.\n# @PRE: payload is a JSON-decoded API response.\n# @POST: Returns the unwrapped object when present.\ndef _extract_result_payload(payload: dict) -> dict:\n result = payload.get(\"result\")\n if isinstance(result, dict):\n return result\n return payload\n\n\n# #endregion _extract_result_payload\n\n\n# #region _extract_created_id [TYPE Function]\n# @BRIEF Extracts object ID from create/update API response.\n# @PRE: payload is a JSON-decoded API response.\n# @POST: Returns integer object ID or None if missing.\ndef _extract_created_id(payload: dict) -> int | None:\n direct_id = payload.get(\"id\")\n if isinstance(direct_id, int):\n return direct_id\n result = payload.get(\"result\")\n if isinstance(result, dict) and isinstance(result.get(\"id\"), int):\n return int(result[\"id\"])\n return None\n\n\n# #endregion _extract_created_id\n\n\n# #region _generate_unique_name [TYPE Function]\n# @BRIEF Generates globally unique random names for charts/dashboards.\n# @PRE: used_names is mutable set for collision tracking.\n# @POST: Returns a unique string and stores it in used_names.\ndef _generate_unique_name(prefix: str, used_names: set[str], rng: random.Random) -> str:\n adjectives = [\n \"amber\",\n \"rapid\",\n \"frozen\",\n \"delta\",\n \"lunar\",\n \"vector\",\n \"cobalt\",\n \"silent\",\n \"neon\",\n \"solar\",\n ]\n nouns = [\n \"falcon\",\n \"matrix\",\n \"signal\",\n \"harbor\",\n \"stream\",\n \"vertex\",\n \"bridge\",\n \"orbit\",\n \"pulse\",\n \"forge\",\n ]\n while True:\n token = uuid.uuid4().hex[:8]\n candidate = f\"{prefix}_{rng.choice(adjectives)}_{rng.choice(nouns)}_{rng.randint(100, 999)}_{token}\"\n if candidate not in used_names:\n used_names.add(candidate)\n return candidate\n\n\n# #endregion _generate_unique_name\n\n\n# #region _resolve_target_envs [TYPE Function]\n# @BRIEF Resolves requested environment IDs from configuration.\n# @PRE: env_ids is non-empty.\n# @POST: Returns mapping env_id -> configured environment object.\ndef _resolve_target_envs(env_ids: list[str]) -> dict[str, Environment]:\n config_manager = ConfigManager()\n configured = {env.id: env for env in config_manager.get_environments()}\n resolved: dict[str, Environment] = {}\n\n if not configured:\n for config_path in [Path(\"config.json\"), Path(\"backend/config.json\")]:\n if not config_path.exists():\n continue\n try:\n payload = json.loads(config_path.read_text(encoding=\"utf-8\"))\n env_rows = payload.get(\"environments\", [])\n for row in env_rows:\n env = Environment(**row)\n configured[env.id] = env\n except Exception as exc:\n logger.reflect(\n f\"Failed loading environments from {config_path}: {exc}\",\n extra={\"src\": \"_resolve_target_envs\"},\n )\n\n for env_id in env_ids:\n env = configured.get(env_id)\n if env is None:\n raise ValueError(f\"Environment '{env_id}' not found in configuration\")\n resolved[env_id] = env\n\n return resolved\n\n\n# #endregion _resolve_target_envs\n\n\n# #region _build_chart_template_pool [TYPE Function]\n# @BRIEF Builds a pool of source chart templates to clone in one environment.\n# @PRE: Client is authenticated.\n# @POST: Returns non-empty list of chart payload templates.\ndef _build_chart_template_pool(\n client: SupersetClient, pool_size: int, rng: random.Random\n) -> list[dict]:\n list_query = {\n \"page\": 0,\n \"page_size\": 1000,\n \"columns\": [\n \"id\",\n \"slice_name\",\n \"datasource_id\",\n \"datasource_type\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n ],\n }\n rows = client.network.fetch_paginated_data(\n endpoint=\"/chart/\",\n pagination_options={\"base_query\": list_query, \"results_field\": \"result\"},\n )\n\n candidates = [row for row in rows if isinstance(row, dict) and row.get(\"id\")]\n if not candidates:\n raise RuntimeError(\"No source charts available for templating\")\n\n selected = (\n candidates\n if len(candidates) <= pool_size\n else rng.sample(candidates, pool_size)\n )\n templates: list[dict] = []\n\n for row in selected:\n chart_id = int(row[\"id\"])\n detail_payload = client.get_chart(chart_id)\n detail = _extract_result_payload(detail_payload)\n\n datasource_id = detail.get(\"datasource_id\")\n datasource_type = (\n detail.get(\"datasource_type\") or row.get(\"datasource_type\") or \"table\"\n )\n if datasource_id is None:\n continue\n\n params_value = detail.get(\"params\")\n if isinstance(params_value, dict):\n params_value = json.dumps(params_value)\n\n query_context_value = detail.get(\"query_context\")\n if isinstance(query_context_value, dict):\n query_context_value = json.dumps(query_context_value)\n\n templates.append(\n {\n \"datasource_id\": int(datasource_id),\n \"datasource_type\": str(datasource_type),\n \"viz_type\": detail.get(\"viz_type\") or row.get(\"viz_type\"),\n \"params\": params_value,\n \"query_context\": query_context_value,\n }\n )\n\n if not templates:\n raise RuntimeError(\"Could not build templates with datasource metadata\")\n\n return templates\n\n\n# #endregion _build_chart_template_pool\n\n\n# #region seed_superset_load_data [TYPE Function]\n# @BRIEF Creates dashboards and cloned charts for load testing across target environments.\n# @PRE: Target environments must be reachable and authenticated.\n# @POST: Returns execution statistics dictionary.\n# @SIDE_EFFECT: Creates objects in Superset environments.\ndef seed_superset_load_data(args: argparse.Namespace) -> dict:\n seed_trace_id()\n rng = random.Random(args.seed)\n env_map = _resolve_target_envs(args.envs)\n\n clients: dict[str, SupersetClient] = {}\n templates_by_env: dict[str, list[dict]] = {}\n created_dashboards: dict[str, list[int]] = {env_id: [] for env_id in env_map}\n created_charts: dict[str, list[int]] = {env_id: [] for env_id in env_map}\n used_chart_names: set[str] = set()\n used_dashboard_names: set[str] = set()\n\n for env_id, env in env_map.items():\n client = SupersetClient(env)\n client.authenticate()\n clients[env_id] = client\n templates_by_env[env_id] = _build_chart_template_pool(\n client, args.template_pool_size, rng\n )\n logger.reason(\n f\"Environment {env_id}: loaded {len(templates_by_env[env_id])} chart templates\",\n extra={\"src\": \"seed_superset_load_data\"},\n )\n\n errors = 0\n env_ids = list(env_map.keys())\n\n for idx in range(args.dashboards):\n env_id = (\n env_ids[idx % len(env_ids)] if idx < len(env_ids) else rng.choice(env_ids)\n )\n dashboard_title = _generate_unique_name(\"lt_dash\", used_dashboard_names, rng)\n\n if args.dry_run:\n logger.reflect(\n f\"Dry-run dashboard create: env={env_id}, title={dashboard_title}\",\n extra={\"src\": \"seed_superset_load_data\"},\n )\n continue\n\n try:\n payload = {\"dashboard_title\": dashboard_title, \"published\": False}\n created = clients[env_id].network.request(\n \"POST\", \"/dashboard/\", data=json.dumps(payload)\n )\n dashboard_id = _extract_created_id(created)\n if dashboard_id is None:\n raise RuntimeError(f\"Dashboard create response missing id: {created}\")\n created_dashboards[env_id].append(dashboard_id)\n except Exception as exc:\n errors += 1\n logger.explore(f\"Failed creating dashboard in {env_id}: {exc}\", extra={\"src\": \"seed_superset_load_data\"})\n if errors >= args.max_errors:\n raise RuntimeError(\n f\"Stopping due to max errors reached ({errors})\"\n ) from exc\n\n if args.dry_run:\n return {\n \"dry_run\": True,\n \"templates_by_env\": {k: len(v) for k, v in templates_by_env.items()},\n \"charts_target\": args.charts,\n \"dashboards_target\": args.dashboards,\n }\n\n for env_id in env_ids:\n if not created_dashboards[env_id]:\n raise RuntimeError(\n f\"No dashboards created in environment {env_id}; cannot bind charts\"\n )\n\n for index in range(args.charts):\n env_id = rng.choice(env_ids)\n client = clients[env_id]\n template = rng.choice(templates_by_env[env_id])\n dashboard_id = rng.choice(created_dashboards[env_id])\n chart_name = _generate_unique_name(\"lt_chart\", used_chart_names, rng)\n\n payload = {\n \"slice_name\": chart_name,\n \"datasource_id\": template[\"datasource_id\"],\n \"datasource_type\": template[\"datasource_type\"],\n \"dashboards\": [dashboard_id],\n }\n if template.get(\"viz_type\"):\n payload[\"viz_type\"] = template[\"viz_type\"]\n if template.get(\"params\"):\n payload[\"params\"] = template[\"params\"]\n if template.get(\"query_context\"):\n payload[\"query_context\"] = template[\"query_context\"]\n\n try:\n created = client.network.request(\n \"POST\", \"/chart/\", data=json.dumps(payload)\n )\n chart_id = _extract_created_id(created)\n if chart_id is None:\n raise RuntimeError(f\"Chart create response missing id: {created}\")\n created_charts[env_id].append(chart_id)\n\n if (index + 1) % 500 == 0:\n logger.reason(f\"Created {index + 1}/{args.charts} charts\", extra={\"src\": \"seed_superset_load_data\"})\n except Exception as exc:\n errors += 1\n logger.explore(f\"Failed creating chart in {env_id}: {exc}\", extra={\"src\": \"seed_superset_load_data\"})\n if errors >= args.max_errors:\n raise RuntimeError(\n f\"Stopping due to max errors reached ({errors})\"\n ) from exc\n\n return {\n \"dry_run\": False,\n \"errors\": errors,\n \"dashboards\": {env_id: len(ids) for env_id, ids in created_dashboards.items()},\n \"charts\": {env_id: len(ids) for env_id, ids in created_charts.items()},\n \"total_dashboards\": sum(len(ids) for ids in created_dashboards.values()),\n \"total_charts\": sum(len(ids) for ids in created_charts.values()),\n }\n\n\n# #endregion seed_superset_load_data\n\n\n# #region main [TYPE Function]\n# @BRIEF CLI entrypoint for Superset load-test data seeding.\n# @PRE: Command line arguments are valid.\n# @POST: Prints summary and exits with non-zero status on failure.\ndef main() -> None:\n seed_trace_id()\n with belief_scope(\"seed_superset_load_test.main\"):\n args = _parse_args()\n result = seed_superset_load_data(args)\n logger.reflect(\n f\"Result summary: {json.dumps(result, ensure_ascii=True)}\",\n extra={\"src\": \"main\"},\n )\n\n\n# #endregion main\n\n\nif __name__ == \"__main__\":\n main()\n\n# #endregion SeedSupersetLoadTestScript\n" + "body": "# #region SeedSupersetLoadTestScript [C:5] [TYPE Module] [SEMANTICS superset, validate]\n#\n# @BRIEF Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments.\n# @LAYER: Infrastructure\n# @RELATION USES -> [ConfigManager]\n# @RELATION USES -> [SupersetClient]\n# @INVARIANT: Created chart and dashboard names are globally unique for one script run.\n# @DATA_CONTRACT: CLIArgs -> TestDataSummary\n\nimport argparse\nimport json\nimport random\nimport sys\nimport uuid\nfrom pathlib import Path\n\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.config_models import Environment\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\n\n\n# #region _parse_args [TYPE Function]\n# @BRIEF Parses CLI arguments for load-test data generation.\n# @PRE: Script is called from CLI.\n# @POST: Returns validated argument namespace.\ndef _parse_args() -> argparse.Namespace:\n parser = argparse.ArgumentParser(\n description=\"Seed Superset with load-test charts and dashboards\"\n )\n parser.add_argument(\n \"--envs\", nargs=\"+\", default=[\"ss1\", \"ss2\"], help=\"Target environment IDs\"\n )\n parser.add_argument(\n \"--charts\", type=int, default=10000, help=\"Target number of charts to create\"\n )\n parser.add_argument(\n \"--dashboards\",\n type=int,\n default=500,\n help=\"Target number of dashboards to create\",\n )\n parser.add_argument(\n \"--template-pool-size\",\n type=int,\n default=200,\n help=\"How many source charts to sample as templates per env\",\n )\n parser.add_argument(\n \"--seed\", type=int, default=None, help=\"Optional RNG seed for reproducibility\"\n )\n parser.add_argument(\n \"--max-errors\",\n type=int,\n default=100,\n help=\"Stop early if errors exceed this threshold\",\n )\n parser.add_argument(\n \"--dry-run\", action=\"store_true\", help=\"Do not write data, only validate setup\"\n )\n return parser.parse_args()\n\n\n# #endregion _parse_args\n\n\n# #region _extract_result_payload [TYPE Function]\n# @BRIEF Normalizes Superset API payloads that may be wrapped in `result`.\n# @PRE: payload is a JSON-decoded API response.\n# @POST: Returns the unwrapped object when present.\ndef _extract_result_payload(payload: dict) -> dict:\n result = payload.get(\"result\")\n if isinstance(result, dict):\n return result\n return payload\n\n\n# #endregion _extract_result_payload\n\n\n# #region _extract_created_id [TYPE Function]\n# @BRIEF Extracts object ID from create/update API response.\n# @PRE: payload is a JSON-decoded API response.\n# @POST: Returns integer object ID or None if missing.\ndef _extract_created_id(payload: dict) -> int | None:\n direct_id = payload.get(\"id\")\n if isinstance(direct_id, int):\n return direct_id\n result = payload.get(\"result\")\n if isinstance(result, dict) and isinstance(result.get(\"id\"), int):\n return int(result[\"id\"])\n return None\n\n\n# #endregion _extract_created_id\n\n\n# #region _generate_unique_name [TYPE Function]\n# @BRIEF Generates globally unique random names for charts/dashboards.\n# @PRE: used_names is mutable set for collision tracking.\n# @POST: Returns a unique string and stores it in used_names.\ndef _generate_unique_name(prefix: str, used_names: set[str], rng: random.Random) -> str:\n adjectives = [\n \"amber\",\n \"rapid\",\n \"frozen\",\n \"delta\",\n \"lunar\",\n \"vector\",\n \"cobalt\",\n \"silent\",\n \"neon\",\n \"solar\",\n ]\n nouns = [\n \"falcon\",\n \"matrix\",\n \"signal\",\n \"harbor\",\n \"stream\",\n \"vertex\",\n \"bridge\",\n \"orbit\",\n \"pulse\",\n \"forge\",\n ]\n while True:\n token = uuid.uuid4().hex[:8]\n candidate = f\"{prefix}_{rng.choice(adjectives)}_{rng.choice(nouns)}_{rng.randint(100, 999)}_{token}\"\n if candidate not in used_names:\n used_names.add(candidate)\n return candidate\n\n\n# #endregion _generate_unique_name\n\n\n# #region _resolve_target_envs [TYPE Function]\n# @BRIEF Resolves requested environment IDs from configuration.\n# @PRE: env_ids is non-empty.\n# @POST: Returns mapping env_id -> configured environment object.\ndef _resolve_target_envs(env_ids: list[str]) -> dict[str, Environment]:\n config_manager = ConfigManager()\n configured = {env.id: env for env in config_manager.get_environments()}\n resolved: dict[str, Environment] = {}\n\n if not configured:\n for config_path in [Path(\"config.json\"), Path(\"backend/config.json\")]:\n if not config_path.exists():\n continue\n try:\n payload = json.loads(config_path.read_text(encoding=\"utf-8\"))\n env_rows = payload.get(\"environments\", [])\n for row in env_rows:\n env = Environment(**row)\n configured[env.id] = env\n except Exception as exc:\n logger.reflect(\n f\"Failed loading environments from {config_path}: {exc}\",\n extra={\"src\": \"_resolve_target_envs\"},\n )\n\n for env_id in env_ids:\n env = configured.get(env_id)\n if env is None:\n raise ValueError(f\"Environment '{env_id}' not found in configuration\")\n resolved[env_id] = env\n\n return resolved\n\n\n# #endregion _resolve_target_envs\n\n\n# #region _build_chart_template_pool [TYPE Function]\n# @BRIEF Builds a pool of source chart templates to clone in one environment.\n# @PRE: Client is authenticated.\n# @POST: Returns non-empty list of chart payload templates.\ndef _build_chart_template_pool(\n client: SupersetClient, pool_size: int, rng: random.Random\n) -> list[dict]:\n list_query = {\n \"page\": 0,\n \"page_size\": 1000,\n \"columns\": [\n \"id\",\n \"slice_name\",\n \"datasource_id\",\n \"datasource_type\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n ],\n }\n rows = client.network.fetch_paginated_data(\n endpoint=\"/chart/\",\n pagination_options={\"base_query\": list_query, \"results_field\": \"result\"},\n )\n\n candidates = [row for row in rows if isinstance(row, dict) and row.get(\"id\")]\n if not candidates:\n raise RuntimeError(\"No source charts available for templating\")\n\n selected = (\n candidates\n if len(candidates) <= pool_size\n else rng.sample(candidates, pool_size)\n )\n templates: list[dict] = []\n\n for row in selected:\n chart_id = int(row[\"id\"])\n detail_payload = client.get_chart(chart_id)\n detail = _extract_result_payload(detail_payload)\n\n datasource_id = detail.get(\"datasource_id\")\n datasource_type = (\n detail.get(\"datasource_type\") or row.get(\"datasource_type\") or \"table\"\n )\n if datasource_id is None:\n continue\n\n params_value = detail.get(\"params\")\n if isinstance(params_value, dict):\n params_value = json.dumps(params_value)\n\n query_context_value = detail.get(\"query_context\")\n if isinstance(query_context_value, dict):\n query_context_value = json.dumps(query_context_value)\n\n templates.append(\n {\n \"datasource_id\": int(datasource_id),\n \"datasource_type\": str(datasource_type),\n \"viz_type\": detail.get(\"viz_type\") or row.get(\"viz_type\"),\n \"params\": params_value,\n \"query_context\": query_context_value,\n }\n )\n\n if not templates:\n raise RuntimeError(\"Could not build templates with datasource metadata\")\n\n return templates\n\n\n# #endregion _build_chart_template_pool\n\n\n# #region seed_superset_load_data [TYPE Function]\n# @BRIEF Creates dashboards and cloned charts for load testing across target environments.\n# @PRE: Target environments must be reachable and authenticated.\n# @POST: Returns execution statistics dictionary.\n# @SIDE_EFFECT: Creates objects in Superset environments.\ndef seed_superset_load_data(args: argparse.Namespace) -> dict:\n seed_trace_id()\n rng = random.Random(args.seed)\n env_map = _resolve_target_envs(args.envs)\n\n clients: dict[str, SupersetClient] = {}\n templates_by_env: dict[str, list[dict]] = {}\n created_dashboards: dict[str, list[int]] = {env_id: [] for env_id in env_map}\n created_charts: dict[str, list[int]] = {env_id: [] for env_id in env_map}\n used_chart_names: set[str] = set()\n used_dashboard_names: set[str] = set()\n\n for env_id, env in env_map.items():\n client = SupersetClient(env)\n client.authenticate()\n clients[env_id] = client\n templates_by_env[env_id] = _build_chart_template_pool(\n client, args.template_pool_size, rng\n )\n logger.reason(\n f\"Environment {env_id}: loaded {len(templates_by_env[env_id])} chart templates\",\n extra={\"src\": \"seed_superset_load_data\"},\n )\n\n errors = 0\n env_ids = list(env_map.keys())\n\n for idx in range(args.dashboards):\n env_id = (\n env_ids[idx % len(env_ids)] if idx < len(env_ids) else rng.choice(env_ids)\n )\n dashboard_title = _generate_unique_name(\"lt_dash\", used_dashboard_names, rng)\n\n if args.dry_run:\n logger.reflect(\n f\"Dry-run dashboard create: env={env_id}, title={dashboard_title}\",\n extra={\"src\": \"seed_superset_load_data\"},\n )\n continue\n\n try:\n payload = {\"dashboard_title\": dashboard_title, \"published\": False}\n created = clients[env_id].network.request(\n \"POST\", \"/dashboard/\", data=json.dumps(payload)\n )\n dashboard_id = _extract_created_id(created)\n if dashboard_id is None:\n raise RuntimeError(f\"Dashboard create response missing id: {created}\")\n created_dashboards[env_id].append(dashboard_id)\n except Exception as exc:\n errors += 1\n logger.explore(f\"Failed creating dashboard in {env_id}: {exc}\", extra={\"src\": \"seed_superset_load_data\"})\n if errors >= args.max_errors:\n raise RuntimeError(\n f\"Stopping due to max errors reached ({errors})\"\n ) from exc\n\n if args.dry_run:\n return {\n \"dry_run\": True,\n \"templates_by_env\": {k: len(v) for k, v in templates_by_env.items()},\n \"charts_target\": args.charts,\n \"dashboards_target\": args.dashboards,\n }\n\n for env_id in env_ids:\n if not created_dashboards[env_id]:\n raise RuntimeError(\n f\"No dashboards created in environment {env_id}; cannot bind charts\"\n )\n\n for index in range(args.charts):\n env_id = rng.choice(env_ids)\n client = clients[env_id]\n template = rng.choice(templates_by_env[env_id])\n dashboard_id = rng.choice(created_dashboards[env_id])\n chart_name = _generate_unique_name(\"lt_chart\", used_chart_names, rng)\n\n payload = {\n \"slice_name\": chart_name,\n \"datasource_id\": template[\"datasource_id\"],\n \"datasource_type\": template[\"datasource_type\"],\n \"dashboards\": [dashboard_id],\n }\n if template.get(\"viz_type\"):\n payload[\"viz_type\"] = template[\"viz_type\"]\n if template.get(\"params\"):\n payload[\"params\"] = template[\"params\"]\n if template.get(\"query_context\"):\n payload[\"query_context\"] = template[\"query_context\"]\n\n try:\n created = client.network.request(\n \"POST\", \"/chart/\", data=json.dumps(payload)\n )\n chart_id = _extract_created_id(created)\n if chart_id is None:\n raise RuntimeError(f\"Chart create response missing id: {created}\")\n created_charts[env_id].append(chart_id)\n\n if (index + 1) % 500 == 0:\n logger.reason(f\"Created {index + 1}/{args.charts} charts\", extra={\"src\": \"seed_superset_load_data\"})\n except Exception as exc:\n errors += 1\n logger.explore(f\"Failed creating chart in {env_id}: {exc}\", extra={\"src\": \"seed_superset_load_data\"})\n if errors >= args.max_errors:\n raise RuntimeError(\n f\"Stopping due to max errors reached ({errors})\"\n ) from exc\n\n return {\n \"dry_run\": False,\n \"errors\": errors,\n \"dashboards\": {env_id: len(ids) for env_id, ids in created_dashboards.items()},\n \"charts\": {env_id: len(ids) for env_id, ids in created_charts.items()},\n \"total_dashboards\": sum(len(ids) for ids in created_dashboards.values()),\n \"total_charts\": sum(len(ids) for ids in created_charts.values()),\n }\n\n\n# #endregion seed_superset_load_data\n\n\n# #region main [TYPE Function]\n# @BRIEF CLI entrypoint for Superset load-test data seeding.\n# @PRE: Command line arguments are valid.\n# @POST: Prints summary and exits with non-zero status on failure.\ndef main() -> None:\n seed_trace_id()\n with belief_scope(\"seed_superset_load_test.main\"):\n args = _parse_args()\n result = seed_superset_load_data(args)\n logger.reflect(\n f\"Result summary: {json.dumps(result, ensure_ascii=True)}\",\n extra={\"src\": \"main\"},\n )\n\n\n# #endregion main\n\n\nif __name__ == \"__main__\":\n main()\n\n# #endregion SeedSupersetLoadTestScript\n" }, { "contract_id": "_parse_args", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 25, - "end_line": 66, + "start_line": 26, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53421,8 +57324,8 @@ "contract_id": "_extract_result_payload", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 69, - "end_line": 80, + "start_line": 70, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53459,8 +57362,8 @@ "contract_id": "_extract_created_id", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 83, - "end_line": 97, + "start_line": 84, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53497,8 +57400,8 @@ "contract_id": "_generate_unique_name", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 100, - "end_line": 137, + "start_line": 101, + "end_line": 138, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53535,8 +57438,8 @@ "contract_id": "_resolve_target_envs", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 140, - "end_line": 174, + "start_line": 141, + "end_line": 175, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53573,8 +57476,8 @@ "contract_id": "_build_chart_template_pool", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 177, - "end_line": 249, + "start_line": 178, + "end_line": 250, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53611,8 +57514,8 @@ "contract_id": "seed_superset_load_data", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 252, - "end_line": 377, + "start_line": 253, + "end_line": 378, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53659,8 +57562,8 @@ "contract_id": "main", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 380, - "end_line": 395, + "start_line": 381, + "end_line": 396, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53873,12 +57776,55 @@ "tier_source": "AutoCalculated", "body": "# #region CleanReleaseContracts [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, package, subsystem]\n# @BRIEF Publish the canonical semantic root for the clean-release backend service cluster.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [ComplianceOrchestrator]\n# @RELATION DEPENDS_ON -> [ManifestBuilder]\n# @RELATION DEPENDS_ON -> [PolicyEngine]\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n\nfrom ...core.logger import logger\n\n# [REASON] Initializing clean_release package.\nlogger.reason(\"Clean release compliance subsystem initialized.\")\n\n# Legacy compatibility exports are intentionally lazy to avoid import cycles.\n__all__ = [\n \"logger\",\n]\n\n# #endregion CleanReleaseContracts\n" }, + { + "contract_id": "ApprovalService", + "contract_type": "Module", + "file_path": "backend/src/services/clean_release/approval_service.py", + "start_line": 1, + "end_line": 214, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "DATA_CONTRACT": "ApprovalRequest -> ApprovalDecision", + "INVARIANT": "Approval is allowed only for PASSED report bound to candidate; decisions are append-only.", + "LAYER": "Domain", + "POST": "Approval decision appended; candidate lifecycle advanced", + "PRE": "Report with PASSED final_status exists for candidate", + "PURPOSE": "Enforce approval/rejection gates over immutable compliance reports.", + "SIDE_EFFECT": "Persists approval decisions, transitions candidate status" + }, + "relations": [ + { + "source_id": "ApprovalService", + "relation_type": "DEPENDS_ON", + "target_id": "RepositoryRelations", + "target_ref": "[RepositoryRelations]" + }, + { + "source_id": "ApprovalService", + "relation_type": "DEPENDS_ON", + "target_id": "CleanReleaseModels", + "target_ref": "[CleanReleaseModels]" + }, + { + "source_id": "ApprovalService", + "relation_type": "DEPENDS_ON", + "target_id": "AuditService", + "target_ref": "[AuditService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region ApprovalService [C:5] [TYPE Module] [SEMANTICS clean-release, approval, gate, compliance]\n# @BRIEF Enforce approval/rejection gates over immutable compliance reports.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [AuditService]\n# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; decisions are append-only.\n# @PRE: Report with PASSED final_status exists for candidate\n# @POST: Approval decision appended; candidate lifecycle advanced\n# @SIDE_EFFECT: Persists approval decisions, transitions candidate status\n# @DATA_CONTRACT: ApprovalRequest -> ApprovalDecision\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom uuid import uuid4\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import ApprovalDecision\nfrom .audit_service import audit_preparation\nfrom .enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision\nfrom .exceptions import ApprovalGateError\nfrom .repository import CleanReleaseRepository\n\n\n# #region _get_or_init_decisions_store [TYPE Function]\n# @BRIEF Provide append-only in-memory storage for approval decisions.\n# @PRE: repository is initialized.\n# @POST: Returns mutable decision list attached to repository.\ndef _get_or_init_decisions_store(\n repository: CleanReleaseRepository,\n) -> list[ApprovalDecision]:\n decisions = getattr(repository, \"approval_decisions\", None)\n if decisions is None:\n decisions = []\n repository.approval_decisions = decisions\n return decisions\n\n\n# #endregion _get_or_init_decisions_store\n\n\n# #region _latest_decision_for_candidate [TYPE Function]\n# @BRIEF Resolve latest approval decision for candidate from append-only store.\n# @PRE: candidate_id is non-empty.\n# @POST: Returns latest ApprovalDecision or None.\ndef _latest_decision_for_candidate(\n repository: CleanReleaseRepository, candidate_id: str\n) -> ApprovalDecision | None:\n decisions = _get_or_init_decisions_store(repository)\n scoped = [item for item in decisions if item.candidate_id == candidate_id]\n if not scoped:\n return None\n return sorted(\n scoped,\n key=lambda item: item.decided_at or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n\n\n# #endregion _latest_decision_for_candidate\n\n\n# #region _resolve_candidate_and_report [TYPE Function]\n# @BRIEF Validate candidate/report existence and ownership prior to decision persistence.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Returns tuple(candidate, report); raises ApprovalGateError on contract violation.\ndef _resolve_candidate_and_report(\n repository: CleanReleaseRepository,\n *,\n candidate_id: str,\n report_id: str,\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ApprovalGateError(f\"candidate '{candidate_id}' not found\")\n\n report = repository.get_report(report_id)\n if report is None:\n raise ApprovalGateError(f\"report '{report_id}' not found\")\n\n if report.candidate_id != candidate_id:\n raise ApprovalGateError(\"report belongs to another candidate\")\n\n return candidate, report\n\n\n# #endregion _resolve_candidate_and_report\n\n\n# #region approve_candidate [TYPE Function]\n# @BRIEF Persist immutable APPROVED decision and advance candidate lifecycle to APPROVED.\n# @PRE: Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED.\n# @POST: Approval decision is appended and candidate transitions to APPROVED.\ndef approve_candidate(\n *,\n repository: CleanReleaseRepository,\n candidate_id: str,\n report_id: str,\n decided_by: str,\n comment: str | None = None,\n) -> ApprovalDecision:\n with belief_scope(\"approval_service.approve_candidate\"):\n logger.reason(\n f\"Evaluating approve gate candidate_id={candidate_id} report_id={report_id}\"\n )\n\n if not decided_by or not decided_by.strip():\n raise ApprovalGateError(\"decided_by must be non-empty\")\n\n candidate, report = _resolve_candidate_and_report(\n repository,\n candidate_id=candidate_id,\n report_id=report_id,\n )\n\n if report.final_status != ComplianceDecision.PASSED.value:\n raise ApprovalGateError(\"approve requires PASSED compliance report\")\n\n latest = _latest_decision_for_candidate(repository, candidate_id)\n if (\n latest is not None\n and latest.decision == ApprovalDecisionType.APPROVED.value\n ):\n raise ApprovalGateError(\"candidate is already approved\")\n\n if candidate.status == CandidateStatus.APPROVED.value:\n raise ApprovalGateError(\"candidate is already approved\")\n\n try:\n if candidate.status != CandidateStatus.CHECK_PASSED.value:\n raise ApprovalGateError(\n f\"candidate status '{candidate.status}' cannot transition to APPROVED\"\n )\n candidate.transition_to(CandidateStatus.APPROVED)\n repository.save_candidate(candidate)\n except ApprovalGateError:\n raise\n except Exception as exc:\n logger.explore(\n f\"Candidate transition to APPROVED failed candidate_id={candidate_id}: {exc}\"\n )\n raise ApprovalGateError(str(exc)) from exc\n\n decision = ApprovalDecision(\n id=f\"approve-{uuid4()}\",\n candidate_id=candidate_id,\n report_id=report_id,\n decision=ApprovalDecisionType.APPROVED.value,\n decided_by=decided_by,\n decided_at=datetime.now(UTC),\n comment=comment,\n )\n _get_or_init_decisions_store(repository).append(decision)\n audit_preparation(\n candidate_id, \"APPROVED\", repository=repository, actor=decided_by\n )\n logger.reflect(\n f\"Approval persisted candidate_id={candidate_id} decision_id={decision.id}\"\n )\n return decision\n\n\n# #endregion approve_candidate\n\n\n# #region reject_candidate [TYPE Function]\n# @BRIEF Persist immutable REJECTED decision without promoting candidate lifecycle.\n# @PRE: Candidate exists and report belongs to candidate.\n# @POST: Rejected decision is appended; candidate lifecycle is unchanged.\ndef reject_candidate(\n *,\n repository: CleanReleaseRepository,\n candidate_id: str,\n report_id: str,\n decided_by: str,\n comment: str | None = None,\n) -> ApprovalDecision:\n with belief_scope(\"approval_service.reject_candidate\"):\n logger.reason(\n f\"Evaluating reject decision candidate_id={candidate_id} report_id={report_id}\"\n )\n\n if not decided_by or not decided_by.strip():\n raise ApprovalGateError(\"decided_by must be non-empty\")\n\n _resolve_candidate_and_report(\n repository,\n candidate_id=candidate_id,\n report_id=report_id,\n )\n\n decision = ApprovalDecision(\n id=f\"reject-{uuid4()}\",\n candidate_id=candidate_id,\n report_id=report_id,\n decision=ApprovalDecisionType.REJECTED.value,\n decided_by=decided_by,\n decided_at=datetime.now(UTC),\n comment=comment,\n )\n _get_or_init_decisions_store(repository).append(decision)\n audit_preparation(\n candidate_id, \"REJECTED\", repository=repository, actor=decided_by\n )\n logger.reflect(\n f\"Rejection persisted candidate_id={candidate_id} decision_id={decision.id}\"\n )\n return decision\n\n\n# #endregion reject_candidate\n\n# #endregion ApprovalService\n" + }, { "contract_id": "_get_or_init_decisions_store", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 22, - "end_line": 36, + "start_line": 26, + "end_line": 40, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53915,8 +57861,8 @@ "contract_id": "_latest_decision_for_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 39, - "end_line": 57, + "start_line": 43, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53953,8 +57899,8 @@ "contract_id": "_resolve_candidate_and_report", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 60, - "end_line": 84, + "start_line": 64, + "end_line": 88, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53991,8 +57937,8 @@ "contract_id": "approve_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 87, - "end_line": 160, + "start_line": 91, + "end_line": 164, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -54029,8 +57975,8 @@ "contract_id": "reject_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 163, - "end_line": 208, + "start_line": 167, + "end_line": 212, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -54063,12 +58009,60 @@ "tier_source": "AutoCalculated", "body": "# #region reject_candidate [TYPE Function]\n# @BRIEF Persist immutable REJECTED decision without promoting candidate lifecycle.\n# @PRE: Candidate exists and report belongs to candidate.\n# @POST: Rejected decision is appended; candidate lifecycle is unchanged.\ndef reject_candidate(\n *,\n repository: CleanReleaseRepository,\n candidate_id: str,\n report_id: str,\n decided_by: str,\n comment: str | None = None,\n) -> ApprovalDecision:\n with belief_scope(\"approval_service.reject_candidate\"):\n logger.reason(\n f\"Evaluating reject decision candidate_id={candidate_id} report_id={report_id}\"\n )\n\n if not decided_by or not decided_by.strip():\n raise ApprovalGateError(\"decided_by must be non-empty\")\n\n _resolve_candidate_and_report(\n repository,\n candidate_id=candidate_id,\n report_id=report_id,\n )\n\n decision = ApprovalDecision(\n id=f\"reject-{uuid4()}\",\n candidate_id=candidate_id,\n report_id=report_id,\n decision=ApprovalDecisionType.REJECTED.value,\n decided_by=decided_by,\n decided_at=datetime.now(UTC),\n comment=comment,\n )\n _get_or_init_decisions_store(repository).append(decision)\n audit_preparation(\n candidate_id, \"REJECTED\", repository=repository, actor=decided_by\n )\n logger.reflect(\n f\"Rejection persisted candidate_id={candidate_id} decision_id={decision.id}\"\n )\n return decision\n\n\n# #endregion reject_candidate\n" }, + { + "contract_id": "ArtifactCatalogLoader", + "contract_type": "Module", + "file_path": "backend/src/services/clean_release/artifact_catalog_loader.py", + "start_line": 1, + "end_line": 106, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "DATA_CONTRACT": "FilePath -> CandidateArtifact[]", + "INVARIANT": "Artifact catalog must produce deterministic CandidateArtifact entries with required identity and checksum fields.", + "LAYER": "Domain", + "PURPOSE": "Load bootstrap artifact catalogs for clean release real-mode flows.", + "SIDE_EFFECT": "Reads JSON file from filesystem" + }, + "relations": [ + { + "source_id": "ArtifactCatalogLoader", + "relation_type": "DEPENDS_ON", + "target_id": "CleanReleaseModels", + "target_ref": "[CleanReleaseModels]" + } + ], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region ArtifactCatalogLoader [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, artifact, catalog, manifest]\n# @BRIEF Load bootstrap artifact catalogs for clean release real-mode flows.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Artifact catalog must produce deterministic CandidateArtifact entries with required identity and checksum fields.\n# @SIDE_EFFECT: Reads JSON file from filesystem\n# @DATA_CONTRACT: FilePath -> CandidateArtifact[]\n\nfrom __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nfrom ...models.clean_release import CandidateArtifact\n\n\n# #region load_bootstrap_artifacts [TYPE Function]\n# @BRIEF Parse artifact catalog JSON into CandidateArtifact models for TUI/bootstrap flows.\n# @PRE: path points to readable JSON file; payload is list[artifact] or {\"artifacts\": list[artifact]}.\n# @POST: Returns non-mutated CandidateArtifact models with required fields populated.\ndef load_bootstrap_artifacts(path: str, candidate_id: str) -> list[CandidateArtifact]:\n if not path or not path.strip():\n return []\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id must be non-empty for artifact catalog import\")\n\n catalog_path = Path(path)\n payload = json.loads(catalog_path.read_text(encoding=\"utf-8\"))\n raw_artifacts = payload.get(\"artifacts\") if isinstance(payload, dict) else payload\n if not isinstance(raw_artifacts, list):\n raise ValueError(\n \"artifact catalog must be a list or an object with 'artifacts' list\"\n )\n\n artifacts: list[CandidateArtifact] = []\n for index, raw_artifact in enumerate(raw_artifacts, start=1):\n if not isinstance(raw_artifact, dict):\n raise ValueError(f\"artifact #{index} must be an object\")\n\n artifact_id = str(raw_artifact.get(\"id\", \"\")).strip()\n artifact_path = str(raw_artifact.get(\"path\", \"\")).strip()\n artifact_sha256 = str(raw_artifact.get(\"sha256\", \"\")).strip()\n artifact_size = raw_artifact.get(\"size\")\n if not artifact_id:\n raise ValueError(f\"artifact #{index} missing required field 'id'\")\n if not artifact_path:\n raise ValueError(f\"artifact #{index} missing required field 'path'\")\n if not artifact_sha256:\n raise ValueError(f\"artifact #{index} missing required field 'sha256'\")\n if not isinstance(artifact_size, int) or artifact_size < 0:\n raise ValueError(\n f\"artifact #{index} field 'size' must be non-negative integer\"\n )\n\n category = (\n str(\n raw_artifact.get(\"detected_category\")\n or raw_artifact.get(\"category\")\n or \"\"\n ).strip()\n or None\n )\n source_uri = str(raw_artifact.get(\"source_uri\", \"\")).strip() or None\n source_host = str(raw_artifact.get(\"source_host\", \"\")).strip() or None\n metadata_json = raw_artifact.get(\"metadata_json\")\n if metadata_json is None:\n metadata_json = {\n key: value\n for key, value in raw_artifact.items()\n if key\n not in {\n \"id\",\n \"path\",\n \"sha256\",\n \"size\",\n \"category\",\n \"detected_category\",\n \"source_uri\",\n \"source_host\",\n \"metadata_json\",\n }\n }\n if not isinstance(metadata_json, dict):\n raise ValueError(f\"artifact #{index} field 'metadata_json' must be object\")\n\n artifacts.append(\n CandidateArtifact(\n id=artifact_id,\n candidate_id=candidate_id,\n path=artifact_path,\n sha256=artifact_sha256,\n size=artifact_size,\n detected_category=category,\n declared_category=category,\n source_uri=source_uri,\n source_host=source_host,\n metadata_json=metadata_json,\n )\n )\n\n return artifacts\n\n\n# #endregion load_bootstrap_artifacts\n\n# #endregion ArtifactCatalogLoader\n" + }, { "contract_id": "load_bootstrap_artifacts", "contract_type": "Function", "file_path": "backend/src/services/clean_release/artifact_catalog_loader.py", - "start_line": 15, - "end_line": 102, + "start_line": 17, + "end_line": 104, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -54101,6 +58095,137 @@ "tier_source": "AutoCalculated", "body": "# #region load_bootstrap_artifacts [TYPE Function]\n# @BRIEF Parse artifact catalog JSON into CandidateArtifact models for TUI/bootstrap flows.\n# @PRE: path points to readable JSON file; payload is list[artifact] or {\"artifacts\": list[artifact]}.\n# @POST: Returns non-mutated CandidateArtifact models with required fields populated.\ndef load_bootstrap_artifacts(path: str, candidate_id: str) -> list[CandidateArtifact]:\n if not path or not path.strip():\n return []\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id must be non-empty for artifact catalog import\")\n\n catalog_path = Path(path)\n payload = json.loads(catalog_path.read_text(encoding=\"utf-8\"))\n raw_artifacts = payload.get(\"artifacts\") if isinstance(payload, dict) else payload\n if not isinstance(raw_artifacts, list):\n raise ValueError(\n \"artifact catalog must be a list or an object with 'artifacts' list\"\n )\n\n artifacts: list[CandidateArtifact] = []\n for index, raw_artifact in enumerate(raw_artifacts, start=1):\n if not isinstance(raw_artifact, dict):\n raise ValueError(f\"artifact #{index} must be an object\")\n\n artifact_id = str(raw_artifact.get(\"id\", \"\")).strip()\n artifact_path = str(raw_artifact.get(\"path\", \"\")).strip()\n artifact_sha256 = str(raw_artifact.get(\"sha256\", \"\")).strip()\n artifact_size = raw_artifact.get(\"size\")\n if not artifact_id:\n raise ValueError(f\"artifact #{index} missing required field 'id'\")\n if not artifact_path:\n raise ValueError(f\"artifact #{index} missing required field 'path'\")\n if not artifact_sha256:\n raise ValueError(f\"artifact #{index} missing required field 'sha256'\")\n if not isinstance(artifact_size, int) or artifact_size < 0:\n raise ValueError(\n f\"artifact #{index} field 'size' must be non-negative integer\"\n )\n\n category = (\n str(\n raw_artifact.get(\"detected_category\")\n or raw_artifact.get(\"category\")\n or \"\"\n ).strip()\n or None\n )\n source_uri = str(raw_artifact.get(\"source_uri\", \"\")).strip() or None\n source_host = str(raw_artifact.get(\"source_host\", \"\")).strip() or None\n metadata_json = raw_artifact.get(\"metadata_json\")\n if metadata_json is None:\n metadata_json = {\n key: value\n for key, value in raw_artifact.items()\n if key\n not in {\n \"id\",\n \"path\",\n \"sha256\",\n \"size\",\n \"category\",\n \"detected_category\",\n \"source_uri\",\n \"source_host\",\n \"metadata_json\",\n }\n }\n if not isinstance(metadata_json, dict):\n raise ValueError(f\"artifact #{index} field 'metadata_json' must be object\")\n\n artifacts.append(\n CandidateArtifact(\n id=artifact_id,\n candidate_id=candidate_id,\n path=artifact_path,\n sha256=artifact_sha256,\n size=artifact_size,\n detected_category=category,\n declared_category=category,\n source_uri=source_uri,\n source_host=source_host,\n metadata_json=metadata_json,\n )\n )\n\n return artifacts\n\n\n# #endregion load_bootstrap_artifacts\n" }, + { + "contract_id": "AuditService", + "contract_type": "Module", + "file_path": "backend/src/services/clean_release/audit_service.py", + "start_line": 1, + "end_line": 121, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "DATA_CONTRACT": "AuditAction -> LogEntry", + "INVARIANT": "Audit hooks are append-only log actions.", + "LAYER": "Infrastructure", + "POST": "Audit events appended to log", + "PRE": "Logger configured", + "PURPOSE": "Provide lightweight audit hooks for clean release preparation/check/report lifecycle.", + "SIDE_EFFECT": "Writes audit events to logger and repository" + }, + "relations": [ + { + "source_id": "AuditService", + "relation_type": "DEPENDS_ON", + "target_id": "LoggerModule", + "target_ref": "[LoggerModule]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AuditService [C:3] [TYPE Module] [SEMANTICS clean-release, audit, report, trail, release]\n# @BRIEF Provide lightweight audit hooks for clean release preparation/check/report lifecycle.\n# @LAYER: Infrastructure\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Audit hooks are append-only log actions.\n# @PRE: Logger configured\n# @POST: Audit events appended to log\n# @SIDE_EFFECT: Writes audit events to logger and repository\n# @DATA_CONTRACT: AuditAction -> LogEntry\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any\nfrom uuid import uuid4\n\nfrom ...core.logger import logger\n\n\ndef _append_event(repository, payload: dict[str, Any]) -> None:\n if repository is not None and hasattr(repository, \"append_audit_event\"):\n repository.append_audit_event(payload)\n\n\ndef audit_preparation(\n candidate_id: str, status: str, repository=None, actor: str = \"system\"\n) -> None:\n logger.reason(\n f\"clean-release preparation candidate={candidate_id} status={status}\"\n )\n _append_event(\n repository,\n {\n \"id\": f\"audit-{uuid4()}\",\n \"action\": \"PREPARATION\",\n \"candidate_id\": candidate_id,\n \"actor\": actor,\n \"status\": status,\n \"timestamp\": datetime.now(UTC).isoformat(),\n },\n )\n\n\ndef audit_check_run(\n check_run_id: str,\n final_status: str,\n repository=None,\n *,\n candidate_id: str | None = None,\n actor: str = \"system\",\n) -> None:\n logger.reflect(\n f\"clean-release check_run={check_run_id} final_status={final_status}\"\n )\n _append_event(\n repository,\n {\n \"id\": f\"audit-{uuid4()}\",\n \"action\": \"CHECK_RUN\",\n \"run_id\": check_run_id,\n \"candidate_id\": candidate_id,\n \"actor\": actor,\n \"status\": final_status,\n \"timestamp\": datetime.now(UTC).isoformat(),\n },\n )\n\n\ndef audit_violation(\n run_id: str,\n stage_name: str,\n code: str,\n repository=None,\n *,\n candidate_id: str | None = None,\n actor: str = \"system\",\n) -> None:\n logger.explore(\n f\"clean-release violation run_id={run_id} stage={stage_name} code={code}\"\n )\n _append_event(\n repository,\n {\n \"id\": f\"audit-{uuid4()}\",\n \"action\": \"VIOLATION\",\n \"run_id\": run_id,\n \"candidate_id\": candidate_id,\n \"actor\": actor,\n \"stage_name\": stage_name,\n \"code\": code,\n \"timestamp\": datetime.now(UTC).isoformat(),\n },\n )\n\n\ndef audit_report(\n report_id: str,\n candidate_id: str,\n repository=None,\n *,\n run_id: str | None = None,\n actor: str = \"system\",\n) -> None:\n logger.explore(\n f\"clean-release report_id={report_id} candidate={candidate_id}\"\n )\n _append_event(\n repository,\n {\n \"id\": f\"audit-{uuid4()}\",\n \"action\": \"REPORT\",\n \"report_id\": report_id,\n \"run_id\": run_id,\n \"candidate_id\": candidate_id,\n \"actor\": actor,\n \"timestamp\": datetime.now(UTC).isoformat(),\n },\n )\n\n\n# #endregion AuditService\n" + }, + { + "contract_id": "candidate_service", + "contract_type": "Module", + "file_path": "backend/src/services/clean_release/candidate_service.py", + "start_line": 1, + "end_line": 106, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "INVARIANT": "Candidate lifecycle transitions are delegated to domain guard logic.", + "LAYER": "Domain", + "POST": "candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only.", + "PRE": "candidate_id must be unique; artifacts input must be non-empty and valid.", + "PURPOSE": "Register release candidates with validated artifacts and advance lifecycle through legal transitions." + }, + "relations": [ + { + "source_id": "candidate_service", + "relation_type": "DEPENDS_ON", + "target_id": "backend.src.services.clean_release.repository", + "target_ref": "backend.src.services.clean_release.repository" + }, + { + "source_id": "candidate_service", + "relation_type": "DEPENDS_ON", + "target_id": "backend.src.models.clean_release", + "target_ref": "backend.src.models.clean_release" + } + ], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], + "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 -> backend.src.services.clean_release.repository\n# @RELATION DEPENDS_ON -> 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" + }, { "contract_id": "_validate_artifacts", "contract_type": "Function", @@ -54395,14 +58520,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/demo_data_service.py", "start_line": 1, - "end_line": 54, - "tier": "TIER_2", - "complexity": 3, + "end_line": 56, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "SeedConfig -> DemoEntities", "INVARIANT": "Demo and real namespaces must never collide for generated physical identifiers.", "LAYER": "Domain", - "PURPOSE": "Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes." + "PURPOSE": "Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.", + "SIDE_EFFECT": "Writes demo entities to repository" }, "relations": [ { @@ -54414,25 +58541,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DemoDataService [C:3] [TYPE Module] [SEMANTICS clean-release, demo, seed, fixture]\n# @BRIEF Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @INVARIANT: Demo and real namespaces must never collide for generated physical identifiers.\n\nfrom __future__ import annotations\n\nfrom .repository import CleanReleaseRepository\n\n\n# #region resolve_namespace [TYPE Function]\n# @BRIEF Resolve canonical clean-release namespace for requested mode.\n# @PRE: mode is a non-empty string identifying runtime mode.\n# @POST: Returns deterministic namespace key for demo/real separation.\ndef resolve_namespace(mode: str) -> str:\n normalized = (mode or \"\").strip().lower()\n if normalized == \"demo\":\n return \"clean-release:demo\"\n return \"clean-release:real\"\n\n\n# #endregion resolve_namespace\n\n\n# #region build_namespaced_id [TYPE Function]\n# @BRIEF Build storage-safe physical identifier under mode namespace.\n# @PRE: namespace and logical_id are non-empty strings.\n# @POST: Returns deterministic \"{namespace}::{logical_id}\" identifier.\ndef build_namespaced_id(namespace: str, logical_id: str) -> str:\n if not namespace or not namespace.strip():\n raise ValueError(\"namespace must be non-empty\")\n if not logical_id or not logical_id.strip():\n raise ValueError(\"logical_id must be non-empty\")\n return f\"{namespace}::{logical_id}\"\n\n\n# #endregion build_namespaced_id\n\n\n# #region create_isolated_repository [TYPE Function]\n# @BRIEF Create isolated in-memory repository instance for selected mode namespace.\n# @PRE: mode is a valid runtime mode marker.\n# @POST: Returns repository instance tagged with namespace metadata.\ndef create_isolated_repository(mode: str) -> CleanReleaseRepository:\n namespace = resolve_namespace(mode)\n repository = CleanReleaseRepository()\n repository.namespace = namespace\n return repository\n\n\n# #endregion create_isolated_repository\n\n# #endregion DemoDataService\n" + "body": "# #region DemoDataService [C:5] [TYPE Module] [SEMANTICS clean-release, demo, seed, fixture]\n# @BRIEF Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @INVARIANT: Demo and real namespaces must never collide for generated physical identifiers.\n# @SIDE_EFFECT: Writes demo entities to repository\n# @DATA_CONTRACT: SeedConfig -> DemoEntities\n\nfrom __future__ import annotations\n\nfrom .repository import CleanReleaseRepository\n\n\n# #region resolve_namespace [TYPE Function]\n# @BRIEF Resolve canonical clean-release namespace for requested mode.\n# @PRE: mode is a non-empty string identifying runtime mode.\n# @POST: Returns deterministic namespace key for demo/real separation.\ndef resolve_namespace(mode: str) -> str:\n normalized = (mode or \"\").strip().lower()\n if normalized == \"demo\":\n return \"clean-release:demo\"\n return \"clean-release:real\"\n\n\n# #endregion resolve_namespace\n\n\n# #region build_namespaced_id [TYPE Function]\n# @BRIEF Build storage-safe physical identifier under mode namespace.\n# @PRE: namespace and logical_id are non-empty strings.\n# @POST: Returns deterministic \"{namespace}::{logical_id}\" identifier.\ndef build_namespaced_id(namespace: str, logical_id: str) -> str:\n if not namespace or not namespace.strip():\n raise ValueError(\"namespace must be non-empty\")\n if not logical_id or not logical_id.strip():\n raise ValueError(\"logical_id must be non-empty\")\n return f\"{namespace}::{logical_id}\"\n\n\n# #endregion build_namespaced_id\n\n\n# #region create_isolated_repository [TYPE Function]\n# @BRIEF Create isolated in-memory repository instance for selected mode namespace.\n# @PRE: mode is a valid runtime mode marker.\n# @POST: Returns repository instance tagged with namespace metadata.\ndef create_isolated_repository(mode: str) -> CleanReleaseRepository:\n namespace = resolve_namespace(mode)\n repository = CleanReleaseRepository()\n repository.namespace = namespace\n return repository\n\n\n# #endregion create_isolated_repository\n\n# #endregion DemoDataService\n" }, { "contract_id": "resolve_namespace", "contract_type": "Function", "file_path": "backend/src/services/clean_release/demo_data_service.py", - "start_line": 12, - "end_line": 23, + "start_line": 14, + "end_line": 25, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -54469,8 +58605,8 @@ "contract_id": "build_namespaced_id", "contract_type": "Function", "file_path": "backend/src/services/clean_release/demo_data_service.py", - "start_line": 26, - "end_line": 38, + "start_line": 28, + "end_line": 40, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -54507,8 +58643,8 @@ "contract_id": "create_isolated_repository", "contract_type": "Function", "file_path": "backend/src/services/clean_release/demo_data_service.py", - "start_line": 41, - "end_line": 52, + "start_line": 43, + "end_line": 54, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -54686,14 +58822,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/manifest_builder.py", "start_line": 1, - "end_line": 134, - "tier": "TIER_2", - "complexity": 3, + "end_line": 136, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ArtifactSet -> Manifest", "INVARIANT": "Equal semantic artifact sets produce identical deterministic hash values.", "LAYER": "Domain", - "PURPOSE": "Build deterministic distribution manifest from classified artifact input." + "PURPOSE": "Build deterministic distribution manifest from classified artifact input.", + "SIDE_EFFECT": "Computes hash of artifact set" }, "relations": [ { @@ -54705,25 +58843,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ManifestBuilder [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, build, artifact, catalog]\n# @BRIEF Build deterministic distribution manifest from classified artifact input.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Equal semantic artifact sets produce identical deterministic hash values.\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nfrom collections.abc import Iterable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...models.clean_release import (\n ClassificationType,\n DistributionManifest,\n ManifestItem,\n ManifestSummary,\n)\n\n\ndef _stable_hash_payload(\n candidate_id: str, policy_id: str, items: list[ManifestItem]\n) -> str:\n serialized = [\n {\n \"path\": item.path,\n \"category\": item.category,\n \"classification\": item.classification.value,\n \"reason\": item.reason,\n \"checksum\": item.checksum,\n }\n for item in sorted(\n items,\n key=lambda i: (\n i.path,\n i.category,\n i.classification.value,\n i.reason,\n i.checksum or \"\",\n ),\n )\n ]\n payload = {\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"items\": serialized,\n }\n digest = hashlib.sha256(\n json.dumps(payload, ensure_ascii=False, sort_keys=True).encode(\"utf-8\")\n ).hexdigest()\n return digest\n\n\n# #region build_distribution_manifest [TYPE Function]\n# @BRIEF Build DistributionManifest with deterministic hash and validated counters.\n# @PRE: artifacts list contains normalized classification values.\n# @POST: Returns DistributionManifest with summary counts matching items cardinality.\ndef build_distribution_manifest(\n manifest_id: str,\n candidate_id: str,\n policy_id: str,\n generated_by: str,\n artifacts: Iterable[dict[str, Any]],\n) -> DistributionManifest:\n items = [\n ManifestItem(\n path=a[\"path\"],\n category=a[\"category\"],\n classification=ClassificationType(a[\"classification\"]),\n reason=a[\"reason\"],\n checksum=a.get(\"checksum\"),\n )\n for a in artifacts\n ]\n\n included_count = sum(\n 1\n for item in items\n if item.classification\n in {ClassificationType.REQUIRED_SYSTEM, ClassificationType.ALLOWED}\n )\n excluded_count = sum(\n 1\n for item in items\n if item.classification == ClassificationType.EXCLUDED_PROHIBITED\n )\n prohibited_detected_count = excluded_count\n\n summary = ManifestSummary(\n included_count=included_count,\n excluded_count=excluded_count,\n prohibited_detected_count=prohibited_detected_count,\n )\n\n deterministic_hash = _stable_hash_payload(candidate_id, policy_id, items)\n\n return DistributionManifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n generated_at=datetime.now(UTC),\n generated_by=generated_by,\n items=items,\n summary=summary,\n deterministic_hash=deterministic_hash,\n )\n\n\n# #endregion build_distribution_manifest\n\n\n# #region build_manifest [TYPE Function]\n# @BRIEF Legacy compatibility wrapper for old manifest builder import paths.\n# @PRE: Same as build_distribution_manifest.\n# @POST: Returns DistributionManifest produced by canonical builder.\ndef build_manifest(\n manifest_id: str,\n candidate_id: str,\n policy_id: str,\n generated_by: str,\n artifacts: Iterable[dict[str, Any]],\n) -> DistributionManifest:\n return build_distribution_manifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n policy_id=policy_id,\n generated_by=generated_by,\n artifacts=artifacts,\n )\n\n\n# #endregion build_manifest\n# #endregion ManifestBuilder\n" + "body": "# #region ManifestBuilder [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, build, artifact, catalog]\n# @BRIEF Build deterministic distribution manifest from classified artifact input.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Equal semantic artifact sets produce identical deterministic hash values.\n# @SIDE_EFFECT: Computes hash of artifact set\n# @DATA_CONTRACT: ArtifactSet -> Manifest\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nfrom collections.abc import Iterable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...models.clean_release import (\n ClassificationType,\n DistributionManifest,\n ManifestItem,\n ManifestSummary,\n)\n\n\ndef _stable_hash_payload(\n candidate_id: str, policy_id: str, items: list[ManifestItem]\n) -> str:\n serialized = [\n {\n \"path\": item.path,\n \"category\": item.category,\n \"classification\": item.classification.value,\n \"reason\": item.reason,\n \"checksum\": item.checksum,\n }\n for item in sorted(\n items,\n key=lambda i: (\n i.path,\n i.category,\n i.classification.value,\n i.reason,\n i.checksum or \"\",\n ),\n )\n ]\n payload = {\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"items\": serialized,\n }\n digest = hashlib.sha256(\n json.dumps(payload, ensure_ascii=False, sort_keys=True).encode(\"utf-8\")\n ).hexdigest()\n return digest\n\n\n# #region build_distribution_manifest [TYPE Function]\n# @BRIEF Build DistributionManifest with deterministic hash and validated counters.\n# @PRE: artifacts list contains normalized classification values.\n# @POST: Returns DistributionManifest with summary counts matching items cardinality.\ndef build_distribution_manifest(\n manifest_id: str,\n candidate_id: str,\n policy_id: str,\n generated_by: str,\n artifacts: Iterable[dict[str, Any]],\n) -> DistributionManifest:\n items = [\n ManifestItem(\n path=a[\"path\"],\n category=a[\"category\"],\n classification=ClassificationType(a[\"classification\"]),\n reason=a[\"reason\"],\n checksum=a.get(\"checksum\"),\n )\n for a in artifacts\n ]\n\n included_count = sum(\n 1\n for item in items\n if item.classification\n in {ClassificationType.REQUIRED_SYSTEM, ClassificationType.ALLOWED}\n )\n excluded_count = sum(\n 1\n for item in items\n if item.classification == ClassificationType.EXCLUDED_PROHIBITED\n )\n prohibited_detected_count = excluded_count\n\n summary = ManifestSummary(\n included_count=included_count,\n excluded_count=excluded_count,\n prohibited_detected_count=prohibited_detected_count,\n )\n\n deterministic_hash = _stable_hash_payload(candidate_id, policy_id, items)\n\n return DistributionManifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n generated_at=datetime.now(UTC),\n generated_by=generated_by,\n items=items,\n summary=summary,\n deterministic_hash=deterministic_hash,\n )\n\n\n# #endregion build_distribution_manifest\n\n\n# #region build_manifest [TYPE Function]\n# @BRIEF Legacy compatibility wrapper for old manifest builder import paths.\n# @PRE: Same as build_distribution_manifest.\n# @POST: Returns DistributionManifest produced by canonical builder.\ndef build_manifest(\n manifest_id: str,\n candidate_id: str,\n policy_id: str,\n generated_by: str,\n artifacts: Iterable[dict[str, Any]],\n) -> DistributionManifest:\n return build_distribution_manifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n policy_id=policy_id,\n generated_by=generated_by,\n artifacts=artifacts,\n )\n\n\n# #endregion build_manifest\n# #endregion ManifestBuilder\n" }, { "contract_id": "build_distribution_manifest", "contract_type": "Function", "file_path": "backend/src/services/clean_release/manifest_builder.py", - "start_line": 56, - "end_line": 110, + "start_line": 58, + "end_line": 112, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -55160,14 +59307,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/preparation_service.py", "start_line": 1, - "end_line": 111, - "tier": "TIER_2", - "complexity": 3, + "end_line": 113, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "PrepareRequest -> PrepareResult", "INVARIANT": "Candidate preparation always persists manifest and candidate status deterministically.", "LAYER": "Domain", - "PURPOSE": "Prepare release candidate by policy evaluation and deterministic manifest creation." + "PURPOSE": "Prepare release candidate by policy evaluation and deterministic manifest creation.", + "SIDE_EFFECT": "Persists candidate and manifest" }, "relations": [ { @@ -55191,25 +59340,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region PreparationService [C:3] [TYPE Module] [SEMANTICS clean-release, prepare, validate, policy, manifest]\n# @BRIEF Prepare release candidate by policy evaluation and deterministic manifest creation.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [PolicyEngine]\n# @RELATION DEPENDS_ON -> [ManifestBuilder]\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom datetime import UTC, datetime\n\nfrom ...models.clean_release import ReleaseCandidateStatus\nfrom .enums import CandidateStatus\nfrom .manifest_builder import build_distribution_manifest\nfrom .policy_engine import CleanPolicyEngine\nfrom .repository import CleanReleaseRepository\n\n\ndef prepare_candidate(\n repository: CleanReleaseRepository,\n candidate_id: str,\n artifacts: Iterable[dict],\n sources: Iterable[str],\n operator_id: str,\n) -> dict:\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ValueError(f\"Candidate not found: {candidate_id}\")\n\n policy = repository.get_active_policy()\n if policy is None:\n raise ValueError(\"Active clean policy not found\")\n\n registry_ref = getattr(policy, \"registry_snapshot_id\", None) or getattr(\n policy, \"internal_source_registry_ref\", None\n )\n registry = repository.get_registry(registry_ref) if registry_ref else None\n if registry is None:\n raise ValueError(\"Registry not found for active policy\")\n\n engine = CleanPolicyEngine(policy=policy, registry=registry)\n validation = engine.validate_policy()\n if not validation.ok:\n raise ValueError(f\"Invalid policy: {validation.blocking_reasons}\")\n\n classified, violations = engine.evaluate_candidate(\n artifacts=artifacts, sources=sources\n )\n\n manifest = build_distribution_manifest(\n manifest_id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n policy_id=getattr(policy, \"policy_id\", None) or getattr(policy, \"id\", \"\"),\n generated_by=operator_id,\n artifacts=classified,\n )\n repository.save_manifest(manifest)\n\n current_status = getattr(candidate, \"status\", None)\n if violations:\n candidate.status = ReleaseCandidateStatus.BLOCKED.value\n repository.save_candidate(candidate)\n response_status = ReleaseCandidateStatus.BLOCKED.value\n else:\n if current_status in {\n CandidateStatus.DRAFT,\n CandidateStatus.DRAFT.value,\n \"DRAFT\",\n }:\n candidate.transition_to(CandidateStatus.PREPARED)\n else:\n candidate.status = ReleaseCandidateStatus.PREPARED.value\n repository.save_candidate(candidate)\n response_status = ReleaseCandidateStatus.PREPARED.value\n\n manifest_id_value = getattr(manifest, \"manifest_id\", None) or getattr(\n manifest, \"id\", \"\"\n )\n return {\n \"candidate_id\": candidate_id,\n \"status\": response_status,\n \"manifest_id\": manifest_id_value,\n \"violations\": violations,\n \"prepared_at\": datetime.now(UTC).isoformat(),\n }\n\n\n# #region prepare_candidate_legacy [TYPE Function]\n# @BRIEF Legacy compatibility wrapper kept for migration period.\n# @PRE: Same as prepare_candidate.\n# @POST: Delegates to canonical prepare_candidate and preserves response shape.\ndef prepare_candidate_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n artifacts: Iterable[dict],\n sources: Iterable[str],\n operator_id: str,\n) -> dict:\n return prepare_candidate(\n repository=repository,\n candidate_id=candidate_id,\n artifacts=artifacts,\n sources=sources,\n operator_id=operator_id,\n )\n\n\n# #endregion prepare_candidate_legacy\n# #endregion PreparationService\n" + "body": "# #region PreparationService [C:5] [TYPE Module] [SEMANTICS clean-release, prepare, validate, policy, manifest]\n# @BRIEF Prepare release candidate by policy evaluation and deterministic manifest creation.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [PolicyEngine]\n# @RELATION DEPENDS_ON -> [ManifestBuilder]\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically.\n# @SIDE_EFFECT: Persists candidate and manifest\n# @DATA_CONTRACT: PrepareRequest -> PrepareResult\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom datetime import UTC, datetime\n\nfrom ...models.clean_release import ReleaseCandidateStatus\nfrom .enums import CandidateStatus\nfrom .manifest_builder import build_distribution_manifest\nfrom .policy_engine import CleanPolicyEngine\nfrom .repository import CleanReleaseRepository\n\n\ndef prepare_candidate(\n repository: CleanReleaseRepository,\n candidate_id: str,\n artifacts: Iterable[dict],\n sources: Iterable[str],\n operator_id: str,\n) -> dict:\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ValueError(f\"Candidate not found: {candidate_id}\")\n\n policy = repository.get_active_policy()\n if policy is None:\n raise ValueError(\"Active clean policy not found\")\n\n registry_ref = getattr(policy, \"registry_snapshot_id\", None) or getattr(\n policy, \"internal_source_registry_ref\", None\n )\n registry = repository.get_registry(registry_ref) if registry_ref else None\n if registry is None:\n raise ValueError(\"Registry not found for active policy\")\n\n engine = CleanPolicyEngine(policy=policy, registry=registry)\n validation = engine.validate_policy()\n if not validation.ok:\n raise ValueError(f\"Invalid policy: {validation.blocking_reasons}\")\n\n classified, violations = engine.evaluate_candidate(\n artifacts=artifacts, sources=sources\n )\n\n manifest = build_distribution_manifest(\n manifest_id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n policy_id=getattr(policy, \"policy_id\", None) or getattr(policy, \"id\", \"\"),\n generated_by=operator_id,\n artifacts=classified,\n )\n repository.save_manifest(manifest)\n\n current_status = getattr(candidate, \"status\", None)\n if violations:\n candidate.status = ReleaseCandidateStatus.BLOCKED.value\n repository.save_candidate(candidate)\n response_status = ReleaseCandidateStatus.BLOCKED.value\n else:\n if current_status in {\n CandidateStatus.DRAFT,\n CandidateStatus.DRAFT.value,\n \"DRAFT\",\n }:\n candidate.transition_to(CandidateStatus.PREPARED)\n else:\n candidate.status = ReleaseCandidateStatus.PREPARED.value\n repository.save_candidate(candidate)\n response_status = ReleaseCandidateStatus.PREPARED.value\n\n manifest_id_value = getattr(manifest, \"manifest_id\", None) or getattr(\n manifest, \"id\", \"\"\n )\n return {\n \"candidate_id\": candidate_id,\n \"status\": response_status,\n \"manifest_id\": manifest_id_value,\n \"violations\": violations,\n \"prepared_at\": datetime.now(UTC).isoformat(),\n }\n\n\n# #region prepare_candidate_legacy [TYPE Function]\n# @BRIEF Legacy compatibility wrapper kept for migration period.\n# @PRE: Same as prepare_candidate.\n# @POST: Delegates to canonical prepare_candidate and preserves response shape.\ndef prepare_candidate_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n artifacts: Iterable[dict],\n sources: Iterable[str],\n operator_id: str,\n) -> dict:\n return prepare_candidate(\n repository=repository,\n candidate_id=candidate_id,\n artifacts=artifacts,\n sources=sources,\n operator_id=operator_id,\n )\n\n\n# #endregion prepare_candidate_legacy\n# #endregion PreparationService\n" }, { "contract_id": "prepare_candidate_legacy", "contract_type": "Function", "file_path": "backend/src/services/clean_release/preparation_service.py", - "start_line": 90, - "end_line": 110, + "start_line": 92, + "end_line": 112, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -55242,6 +59400,88 @@ "tier_source": "AutoCalculated", "body": "# #region prepare_candidate_legacy [TYPE Function]\n# @BRIEF Legacy compatibility wrapper kept for migration period.\n# @PRE: Same as prepare_candidate.\n# @POST: Delegates to canonical prepare_candidate and preserves response shape.\ndef prepare_candidate_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n artifacts: Iterable[dict],\n sources: Iterable[str],\n operator_id: str,\n) -> dict:\n return prepare_candidate(\n repository=repository,\n candidate_id=candidate_id,\n artifacts=artifacts,\n sources=sources,\n operator_id=operator_id,\n )\n\n\n# #endregion prepare_candidate_legacy\n" }, + { + "contract_id": "PublicationService", + "contract_type": "Module", + "file_path": "backend/src/services/clean_release/publication_service.py", + "start_line": 1, + "end_line": 212, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "INVARIANT": "Publication records are append-only snapshots; revoke mutates only publication status for targeted record.", + "LAYER": "Domain", + "PURPOSE": "Enforce publication and revocation gates with append-only publication records." + }, + "relations": [ + { + "source_id": "PublicationService", + "relation_type": "DEPENDS_ON", + "target_id": "RepositoryRelations", + "target_ref": "[RepositoryRelations]" + }, + { + "source_id": "PublicationService", + "relation_type": "DEPENDS_ON", + "target_id": "ApprovalService", + "target_ref": "[ApprovalService]" + }, + { + "source_id": "PublicationService", + "relation_type": "DEPENDS_ON", + "target_id": "CleanReleaseModels", + "target_ref": "[CleanReleaseModels]" + }, + { + "source_id": "PublicationService", + "relation_type": "DEPENDS_ON", + "target_id": "AuditService", + "target_ref": "[AuditService]" + } + ], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region PublicationService [C:5] [TYPE Module] [SEMANTICS clean-release, publication, publish, release]\n# @BRIEF Enforce publication and revocation gates with append-only publication records.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @RELATION DEPENDS_ON -> [ApprovalService]\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [AuditService]\n# @INVARIANT: Publication records are append-only snapshots; revoke mutates only publication status for targeted record.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom uuid import uuid4\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import PublicationRecord\nfrom .audit_service import audit_preparation\nfrom .enums import ApprovalDecisionType, CandidateStatus, PublicationStatus\nfrom .exceptions import PublicationGateError\nfrom .repository import CleanReleaseRepository\n\n\n# #region _get_or_init_publications_store [TYPE Function]\n# @BRIEF Provide in-memory append-only publication storage.\n# @PRE: repository is initialized.\n# @POST: Returns publication list attached to repository.\ndef _get_or_init_publications_store(\n repository: CleanReleaseRepository,\n) -> list[PublicationRecord]:\n publications = getattr(repository, \"publication_records\", None)\n if publications is None:\n publications = []\n repository.publication_records = publications\n return publications\n\n\n# #endregion _get_or_init_publications_store\n\n\n# #region _latest_publication_for_candidate [TYPE Function]\n# @BRIEF Resolve latest publication record for candidate.\n# @PRE: candidate_id is non-empty.\n# @POST: Returns latest record or None.\ndef _latest_publication_for_candidate(\n repository: CleanReleaseRepository,\n candidate_id: str,\n) -> PublicationRecord | None:\n records = [\n item\n for item in _get_or_init_publications_store(repository)\n if item.candidate_id == candidate_id\n ]\n if not records:\n return None\n return sorted(\n records,\n key=lambda item: item.published_at or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n\n\n# #endregion _latest_publication_for_candidate\n\n\n# #region _latest_approval_for_candidate [TYPE Function]\n# @BRIEF Resolve latest approval decision from repository decision store.\n# @PRE: candidate_id is non-empty.\n# @POST: Returns latest decision object or None.\ndef _latest_approval_for_candidate(\n repository: CleanReleaseRepository, candidate_id: str\n):\n decisions = getattr(repository, \"approval_decisions\", [])\n scoped = [item for item in decisions if item.candidate_id == candidate_id]\n if not scoped:\n return None\n return sorted(\n scoped,\n key=lambda item: item.decided_at or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n\n\n# #endregion _latest_approval_for_candidate\n\n\n# #region publish_candidate [TYPE Function]\n# @BRIEF Create immutable publication record for approved candidate.\n# @PRE: Candidate exists, report belongs to candidate, latest approval is APPROVED.\n# @POST: New ACTIVE publication record is appended.\ndef publish_candidate(\n *,\n repository: CleanReleaseRepository,\n candidate_id: str,\n report_id: str,\n published_by: str,\n target_channel: str,\n publication_ref: str | None = None,\n) -> PublicationRecord:\n with belief_scope(\"publication_service.publish_candidate\"):\n logger.reason(\n f\"Evaluating publish gate candidate_id={candidate_id} report_id={report_id}\"\n )\n\n if not published_by or not published_by.strip():\n raise PublicationGateError(\"published_by must be non-empty\")\n if not target_channel or not target_channel.strip():\n raise PublicationGateError(\"target_channel must be non-empty\")\n\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise PublicationGateError(f\"candidate '{candidate_id}' not found\")\n\n report = repository.get_report(report_id)\n if report is None:\n raise PublicationGateError(f\"report '{report_id}' not found\")\n if report.candidate_id != candidate_id:\n raise PublicationGateError(\"report belongs to another candidate\")\n\n latest_approval = _latest_approval_for_candidate(repository, candidate_id)\n if (\n latest_approval is None\n or latest_approval.decision != ApprovalDecisionType.APPROVED.value\n ):\n raise PublicationGateError(\"publish requires APPROVED decision\")\n\n latest_publication = _latest_publication_for_candidate(repository, candidate_id)\n if (\n latest_publication is not None\n and latest_publication.status == PublicationStatus.ACTIVE.value\n ):\n raise PublicationGateError(\"candidate already has active publication\")\n\n if candidate.status == CandidateStatus.APPROVED.value:\n try:\n candidate.transition_to(CandidateStatus.PUBLISHED)\n repository.save_candidate(candidate)\n except Exception as exc:\n logger.explore(\n f\"Candidate transition to PUBLISHED failed candidate_id={candidate_id}: {exc}\"\n )\n raise PublicationGateError(str(exc)) from exc\n\n record = PublicationRecord(\n id=f\"pub-{uuid4()}\",\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=published_by,\n published_at=datetime.now(UTC),\n target_channel=target_channel,\n publication_ref=publication_ref,\n status=PublicationStatus.ACTIVE.value,\n )\n _get_or_init_publications_store(repository).append(record)\n audit_preparation(\n candidate_id, \"PUBLISHED\", repository=repository, actor=published_by\n )\n logger.reflect(\n f\"Publication persisted candidate_id={candidate_id} publication_id={record.id}\"\n )\n return record\n\n\n# #endregion publish_candidate\n\n\n# #region revoke_publication [TYPE Function]\n# @BRIEF Revoke existing publication record without deleting history.\n# @PRE: publication_id exists in repository publication store.\n# @POST: Target publication status becomes REVOKED and updated record is returned.\ndef revoke_publication(\n *,\n repository: CleanReleaseRepository,\n publication_id: str,\n revoked_by: str,\n comment: str | None = None,\n) -> PublicationRecord:\n with belief_scope(\"publication_service.revoke_publication\"):\n logger.reason(\n f\"Evaluating revoke gate publication_id={publication_id}\"\n )\n\n if not revoked_by or not revoked_by.strip():\n raise PublicationGateError(\"revoked_by must be non-empty\")\n if not publication_id or not publication_id.strip():\n raise PublicationGateError(\"publication_id must be non-empty\")\n\n records = _get_or_init_publications_store(repository)\n record = next((item for item in records if item.id == publication_id), None)\n if record is None:\n raise PublicationGateError(f\"publication '{publication_id}' not found\")\n if record.status == PublicationStatus.REVOKED.value:\n raise PublicationGateError(\"publication is already revoked\")\n\n record.status = PublicationStatus.REVOKED.value\n candidate = repository.get_candidate(record.candidate_id)\n if candidate is not None:\n # Lifecycle remains publication-driven; republish after revoke is supported by new publication record.\n repository.save_candidate(candidate)\n\n audit_preparation(\n record.candidate_id,\n f\"REVOKED:{comment or ''}\".strip(\":\"),\n repository=repository,\n actor=revoked_by,\n )\n logger.reflect(f\"Publication revoked publication_id={publication_id}\")\n return record\n\n\n# #endregion revoke_publication\n\n# #endregion PublicationService\n" + }, { "contract_id": "_get_or_init_publications_store", "contract_type": "Function", @@ -56005,14 +60245,18 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/repository.py", "start_line": 1, - "end_line": 178, - "tier": "TIER_2", - "complexity": 3, + "end_line": 182, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Entity -> RepositoryOperation", "INVARIANT": "Repository operations are side-effect free outside explicit save/update calls.", - "LAYER": "Infra", - "PURPOSE": "Provide repository adapter for clean release entities with deterministic access methods." + "LAYER": "Infrastructure", + "POST": "Repository operations exported", + "PRE": "In-memory storage initialized", + "PURPOSE": "Provide repository adapter for clean release entities with deterministic access methods.", + "SIDE_EFFECT": "Modifies in-memory state on save/update" }, "relations": [ { @@ -56022,44 +60266,17 @@ "target_ref": "[CleanReleaseModels]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Infra" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region RepositoryRelations [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, repository, relations, release]\n# @BRIEF Provide repository adapter for clean release entities with deterministic access methods.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Repository operations are side-effect free outside explicit save/update calls.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any, cast\n\nfrom ...models.clean_release import (\n CleanPolicySnapshot,\n ComplianceReport,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\n\n\n# #region CleanReleaseRepository [TYPE Class]\n# @BRIEF Data access object for clean release lifecycle.\n@dataclass\nclass CleanReleaseRepository:\n candidates: dict[str, ReleaseCandidate] = field(default_factory=dict)\n policies: dict[str, CleanPolicySnapshot] = field(default_factory=dict)\n registries: dict[str, SourceRegistrySnapshot] = field(default_factory=dict)\n artifacts: dict[str, object] = field(default_factory=dict)\n manifests: dict[str, DistributionManifest] = field(default_factory=dict)\n check_runs: dict[str, ComplianceRun] = field(default_factory=dict)\n stage_runs: dict[str, ComplianceStageRun] = field(default_factory=dict)\n reports: dict[str, ComplianceReport] = field(default_factory=dict)\n violations: dict[str, ComplianceViolation] = field(default_factory=dict)\n audit_events: list[dict[str, Any]] = field(default_factory=list)\n\n def _entity_id(self, entity: Any) -> str:\n return cast(str, str(cast(Any, entity).id))\n\n def _candidate_ref(self, entity: Any) -> str:\n return cast(str, str(cast(Any, entity).candidate_id))\n\n def _run_ref(self, entity: Any) -> str:\n return cast(str, str(cast(Any, entity).run_id))\n\n def save_candidate(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n cast(dict[str, ReleaseCandidate], self.candidates)[\n self._entity_id(candidate)\n ] = candidate\n return candidate\n\n def get_candidate(self, candidate_id: str) -> ReleaseCandidate | None:\n return self.candidates.get(candidate_id)\n\n def save_policy(self, policy: CleanPolicySnapshot) -> CleanPolicySnapshot:\n cast(dict[str, CleanPolicySnapshot], self.policies)[self._entity_id(policy)] = (\n policy\n )\n return policy\n\n def get_policy(self, policy_id: str) -> CleanPolicySnapshot | None:\n return self.policies.get(policy_id)\n\n def get_active_policy(self) -> CleanPolicySnapshot | None:\n # In-memory repo doesn't track 'active' flag on snapshot,\n # this should be resolved by facade using ConfigManager.\n return next(iter(self.policies.values()), None)\n\n def save_registry(self, registry: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n cast(dict[str, SourceRegistrySnapshot], self.registries)[\n self._entity_id(registry)\n ] = registry\n return registry\n\n def get_registry(self, registry_id: str) -> SourceRegistrySnapshot | None:\n return self.registries.get(registry_id)\n\n def save_artifact(self, artifact) -> object:\n cast(dict[str, object], self.artifacts)[self._entity_id(artifact)] = artifact\n return artifact\n\n def get_artifacts_by_candidate(self, candidate_id: str) -> list[object]:\n artifacts = cast(list[Any], list(self.artifacts.values()))\n return [\n artifact\n for artifact in artifacts\n if self._candidate_ref(artifact) == candidate_id\n ]\n\n def save_manifest(self, manifest: DistributionManifest) -> DistributionManifest:\n cast(dict[str, DistributionManifest], self.manifests)[\n self._entity_id(manifest)\n ] = manifest\n return manifest\n\n def get_manifest(self, manifest_id: str) -> DistributionManifest | None:\n return self.manifests.get(manifest_id)\n\n def save_distribution_manifest(\n self, manifest: DistributionManifest\n ) -> DistributionManifest:\n return self.save_manifest(manifest)\n\n def get_distribution_manifest(\n self, manifest_id: str\n ) -> DistributionManifest | None:\n return self.get_manifest(manifest_id)\n\n def save_check_run(self, check_run: ComplianceRun) -> ComplianceRun:\n cast(dict[str, ComplianceRun], self.check_runs)[self._entity_id(check_run)] = (\n check_run\n )\n return check_run\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n cast(dict[str, ComplianceStageRun], self.stage_runs)[\n self._entity_id(stage_run)\n ] = stage_run\n return stage_run\n\n def get_check_run(self, check_run_id: str) -> ComplianceRun | None:\n return self.check_runs.get(check_run_id)\n\n def save_compliance_run(self, run: ComplianceRun) -> ComplianceRun:\n return self.save_check_run(run)\n\n def get_compliance_run(self, run_id: str) -> ComplianceRun | None:\n return self.get_check_run(run_id)\n\n def save_report(self, report: ComplianceReport) -> ComplianceReport:\n report_id = self._entity_id(report)\n store = cast(dict[str, ComplianceReport], self.reports)\n existing = store.get(report_id)\n if existing is not None:\n raise ValueError(\n f\"immutable report snapshot already exists for id={report_id}\"\n )\n store[report_id] = report\n return report\n\n def get_report(self, report_id: str) -> ComplianceReport | None:\n return self.reports.get(report_id)\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n cast(dict[str, ComplianceViolation], self.violations)[\n self._entity_id(violation)\n ] = violation\n return violation\n\n def get_violations_by_run(self, run_id: str) -> list[ComplianceViolation]:\n violations = cast(list[ComplianceViolation], list(self.violations.values()))\n return [\n violation for violation in violations if self._run_ref(violation) == run_id\n ]\n\n def get_manifests_by_candidate(\n self, candidate_id: str\n ) -> list[DistributionManifest]:\n manifests = cast(list[DistributionManifest], list(self.manifests.values()))\n return [\n manifest\n for manifest in manifests\n if self._candidate_ref(manifest) == candidate_id\n ]\n\n def append_audit_event(self, payload: dict[str, Any]) -> None:\n self.audit_events.append(payload)\n\n def clear_history(self) -> None:\n self.check_runs.clear()\n self.reports.clear()\n self.violations.clear()\n\n\n# #endregion CleanReleaseRepository\n# #endregion RepositoryRelations\n" + "body": "# #region RepositoryRelations [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, repository, relations, release]\n# @BRIEF Provide repository adapter for clean release entities with deterministic access methods.\n# @LAYER: Infrastructure\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Repository operations are side-effect free outside explicit save/update calls.\n# @PRE: In-memory storage initialized\n# @POST: Repository operations exported\n# @SIDE_EFFECT: Modifies in-memory state on save/update\n# @DATA_CONTRACT: Entity -> RepositoryOperation\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any, cast\n\nfrom ...models.clean_release import (\n CleanPolicySnapshot,\n ComplianceReport,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\n\n\n# #region CleanReleaseRepository [TYPE Class]\n# @BRIEF Data access object for clean release lifecycle.\n@dataclass\nclass CleanReleaseRepository:\n candidates: dict[str, ReleaseCandidate] = field(default_factory=dict)\n policies: dict[str, CleanPolicySnapshot] = field(default_factory=dict)\n registries: dict[str, SourceRegistrySnapshot] = field(default_factory=dict)\n artifacts: dict[str, object] = field(default_factory=dict)\n manifests: dict[str, DistributionManifest] = field(default_factory=dict)\n check_runs: dict[str, ComplianceRun] = field(default_factory=dict)\n stage_runs: dict[str, ComplianceStageRun] = field(default_factory=dict)\n reports: dict[str, ComplianceReport] = field(default_factory=dict)\n violations: dict[str, ComplianceViolation] = field(default_factory=dict)\n audit_events: list[dict[str, Any]] = field(default_factory=list)\n\n def _entity_id(self, entity: Any) -> str:\n return cast(str, str(cast(Any, entity).id))\n\n def _candidate_ref(self, entity: Any) -> str:\n return cast(str, str(cast(Any, entity).candidate_id))\n\n def _run_ref(self, entity: Any) -> str:\n return cast(str, str(cast(Any, entity).run_id))\n\n def save_candidate(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n cast(dict[str, ReleaseCandidate], self.candidates)[\n self._entity_id(candidate)\n ] = candidate\n return candidate\n\n def get_candidate(self, candidate_id: str) -> ReleaseCandidate | None:\n return self.candidates.get(candidate_id)\n\n def save_policy(self, policy: CleanPolicySnapshot) -> CleanPolicySnapshot:\n cast(dict[str, CleanPolicySnapshot], self.policies)[self._entity_id(policy)] = (\n policy\n )\n return policy\n\n def get_policy(self, policy_id: str) -> CleanPolicySnapshot | None:\n return self.policies.get(policy_id)\n\n def get_active_policy(self) -> CleanPolicySnapshot | None:\n # In-memory repo doesn't track 'active' flag on snapshot,\n # this should be resolved by facade using ConfigManager.\n return next(iter(self.policies.values()), None)\n\n def save_registry(self, registry: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n cast(dict[str, SourceRegistrySnapshot], self.registries)[\n self._entity_id(registry)\n ] = registry\n return registry\n\n def get_registry(self, registry_id: str) -> SourceRegistrySnapshot | None:\n return self.registries.get(registry_id)\n\n def save_artifact(self, artifact) -> object:\n cast(dict[str, object], self.artifacts)[self._entity_id(artifact)] = artifact\n return artifact\n\n def get_artifacts_by_candidate(self, candidate_id: str) -> list[object]:\n artifacts = cast(list[Any], list(self.artifacts.values()))\n return [\n artifact\n for artifact in artifacts\n if self._candidate_ref(artifact) == candidate_id\n ]\n\n def save_manifest(self, manifest: DistributionManifest) -> DistributionManifest:\n cast(dict[str, DistributionManifest], self.manifests)[\n self._entity_id(manifest)\n ] = manifest\n return manifest\n\n def get_manifest(self, manifest_id: str) -> DistributionManifest | None:\n return self.manifests.get(manifest_id)\n\n def save_distribution_manifest(\n self, manifest: DistributionManifest\n ) -> DistributionManifest:\n return self.save_manifest(manifest)\n\n def get_distribution_manifest(\n self, manifest_id: str\n ) -> DistributionManifest | None:\n return self.get_manifest(manifest_id)\n\n def save_check_run(self, check_run: ComplianceRun) -> ComplianceRun:\n cast(dict[str, ComplianceRun], self.check_runs)[self._entity_id(check_run)] = (\n check_run\n )\n return check_run\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n cast(dict[str, ComplianceStageRun], self.stage_runs)[\n self._entity_id(stage_run)\n ] = stage_run\n return stage_run\n\n def get_check_run(self, check_run_id: str) -> ComplianceRun | None:\n return self.check_runs.get(check_run_id)\n\n def save_compliance_run(self, run: ComplianceRun) -> ComplianceRun:\n return self.save_check_run(run)\n\n def get_compliance_run(self, run_id: str) -> ComplianceRun | None:\n return self.get_check_run(run_id)\n\n def save_report(self, report: ComplianceReport) -> ComplianceReport:\n report_id = self._entity_id(report)\n store = cast(dict[str, ComplianceReport], self.reports)\n existing = store.get(report_id)\n if existing is not None:\n raise ValueError(\n f\"immutable report snapshot already exists for id={report_id}\"\n )\n store[report_id] = report\n return report\n\n def get_report(self, report_id: str) -> ComplianceReport | None:\n return self.reports.get(report_id)\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n cast(dict[str, ComplianceViolation], self.violations)[\n self._entity_id(violation)\n ] = violation\n return violation\n\n def get_violations_by_run(self, run_id: str) -> list[ComplianceViolation]:\n violations = cast(list[ComplianceViolation], list(self.violations.values()))\n return [\n violation for violation in violations if self._run_ref(violation) == run_id\n ]\n\n def get_manifests_by_candidate(\n self, candidate_id: str\n ) -> list[DistributionManifest]:\n manifests = cast(list[DistributionManifest], list(self.manifests.values()))\n return [\n manifest\n for manifest in manifests\n if self._candidate_ref(manifest) == candidate_id\n ]\n\n def append_audit_event(self, payload: dict[str, Any]) -> None:\n self.audit_events.append(payload)\n\n def clear_history(self) -> None:\n self.check_runs.clear()\n self.reports.clear()\n self.violations.clear()\n\n\n# #endregion CleanReleaseRepository\n# #endregion RepositoryRelations\n" }, { "contract_id": "CleanReleaseRepository", "contract_type": "Class", "file_path": "backend/src/services/clean_release/repository.py", - "start_line": 24, - "end_line": 177, + "start_line": 28, + "end_line": 181, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56076,14 +60293,18 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/source_isolation.py", "start_line": 1, - "end_line": 37, - "tier": "TIER_2", - "complexity": 3, + "end_line": 41, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "SourceURL -> ViolationReport", "INVARIANT": "Any endpoint outside enabled registry entries is treated as external-source violation.", "LAYER": "Domain", - "PURPOSE": "Validate that all resource endpoints belong to the approved internal source registry." + "POST": "Source isolation violations identified", + "PRE": "Source registry configured", + "PURPOSE": "Validate that all resource endpoints belong to the approved internal source registry.", + "SIDE_EFFECT": "None (read-only check)" }, "relations": [ { @@ -56093,34 +60314,26 @@ "target_ref": "[CleanReleaseModels]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SourceIsolation [C:3] [TYPE Module] [SEMANTICS clean-release, source, isolation, validate, resource]\n# @BRIEF Validate that all resource endpoints belong to the approved internal source registry.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Any endpoint outside enabled registry entries is treated as external-source violation.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\n\nfrom ...models.clean_release import ResourceSourceRegistry\n\n\ndef validate_internal_sources(\n registry: ResourceSourceRegistry, endpoints: Iterable[str]\n) -> dict:\n allowed_hosts = {\n entry.host.strip().lower() for entry in registry.entries if entry.enabled\n }\n violations: list[dict] = []\n\n for endpoint in endpoints:\n normalized = (endpoint or \"\").strip().lower()\n if not normalized or normalized not in allowed_hosts:\n violations.append(\n {\n \"category\": \"external-source\",\n \"location\": endpoint or \"\",\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n }\n )\n\n return {\"ok\": len(violations) == 0, \"violations\": violations}\n\n\n# #endregion SourceIsolation\n" + "body": "# #region SourceIsolation [C:5] [TYPE Module] [SEMANTICS clean-release, source, isolation, validate, resource]\n# @BRIEF Validate that all resource endpoints belong to the approved internal source registry.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: Any endpoint outside enabled registry entries is treated as external-source violation.\n# @PRE: Source registry configured\n# @POST: Source isolation violations identified\n# @SIDE_EFFECT: None (read-only check)\n# @DATA_CONTRACT: SourceURL -> ViolationReport\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\n\nfrom ...models.clean_release import ResourceSourceRegistry\n\n\ndef validate_internal_sources(\n registry: ResourceSourceRegistry, endpoints: Iterable[str]\n) -> dict:\n allowed_hosts = {\n entry.host.strip().lower() for entry in registry.entries if entry.enabled\n }\n violations: list[dict] = []\n\n for endpoint in endpoints:\n normalized = (endpoint or \"\").strip().lower()\n if not normalized or normalized not in allowed_hosts:\n violations.append(\n {\n \"category\": \"external-source\",\n \"location\": endpoint or \"\",\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n }\n )\n\n return {\"ok\": len(violations) == 0, \"violations\": violations}\n\n\n# #endregion SourceIsolation\n" }, { "contract_id": "ComplianceStages", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/__init__.py", "start_line": 1, - "end_line": 125, - "tier": "TIER_2", - "complexity": 3, + "end_line": 127, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "StagePipeline -> ComplianceResult", "INVARIANT": "Stage order remains deterministic for all compliance runs.", "LAYER": "Domain", - "PURPOSE": "Define compliance stage order and helper functions for deterministic run-state evaluation." + "PURPOSE": "Define compliance stage order and helper functions for deterministic run-state evaluation.", + "SIDE_EFFECT": "Registers compliance stages" }, "relations": [ { @@ -56138,25 +60351,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ComplianceStages [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, stage, package]\n# @BRIEF Define compliance stage order and helper functions for deterministic run-state evaluation.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Stage order remains deterministic for all compliance runs.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom typing import Dict, List\n\nfrom ....models.clean_release import (\n CheckFinalStatus,\n CheckStageResult,\n CheckStageStatus,\n ComplianceStageRun,\n)\nfrom ..enums import ComplianceDecision, ComplianceStageName\nfrom .base import ComplianceStage\nfrom .data_purity import DataPurityStage\nfrom .internal_sources_only import InternalSourcesOnlyStage\nfrom .manifest_consistency import ManifestConsistencyStage\nfrom .no_external_endpoints import NoExternalEndpointsStage\n\nMANDATORY_STAGE_ORDER: list[ComplianceStageName] = [\n ComplianceStageName.DATA_PURITY,\n ComplianceStageName.INTERNAL_SOURCES_ONLY,\n ComplianceStageName.NO_EXTERNAL_ENDPOINTS,\n ComplianceStageName.MANIFEST_CONSISTENCY,\n]\n\n\n# #region build_default_stages [TYPE Function]\n# @BRIEF Build default deterministic stage pipeline implementation order.\n# @PRE: None.\n# @POST: Returns stage instances in mandatory execution order.\ndef build_default_stages() -> list[ComplianceStage]:\n return [\n DataPurityStage(),\n InternalSourcesOnlyStage(),\n NoExternalEndpointsStage(),\n ManifestConsistencyStage(),\n ]\n\n\n# #endregion build_default_stages\n\n\n# #region stage_result_map [TYPE Function]\n# @BRIEF Convert stage result list to dictionary by stage name.\n# @PRE: stage_results may be empty or contain unique stage names.\n# @POST: Returns stage->status dictionary for downstream evaluation.\ndef stage_result_map(\n stage_results: Iterable[ComplianceStageRun | CheckStageResult],\n) -> dict[ComplianceStageName, CheckStageStatus]:\n normalized: dict[ComplianceStageName, CheckStageStatus] = {}\n for result in stage_results:\n if isinstance(result, CheckStageResult):\n normalized[ComplianceStageName(result.stage.value)] = CheckStageStatus(\n result.status.value\n )\n continue\n\n stage_name = getattr(result, \"stage_name\", None)\n decision = getattr(result, \"decision\", None)\n status = getattr(result, \"status\", None)\n\n if not stage_name:\n continue\n\n normalized_stage = ComplianceStageName(stage_name)\n if decision == ComplianceDecision.BLOCKED:\n normalized[normalized_stage] = CheckStageStatus.FAIL\n elif decision == ComplianceDecision.ERROR:\n normalized[normalized_stage] = CheckStageStatus.SKIPPED\n elif decision == ComplianceDecision.PASSED:\n normalized[normalized_stage] = CheckStageStatus.PASS\n elif decision:\n normalized[normalized_stage] = CheckStageStatus(str(decision))\n elif status:\n normalized[normalized_stage] = CheckStageStatus(str(status))\n return normalized\n\n\n# #endregion stage_result_map\n\n\n# #region missing_mandatory_stages [TYPE Function]\n# @BRIEF Identify mandatory stages that are absent from run results.\n# @PRE: stage_status_map contains zero or more known stage statuses.\n# @POST: Returns ordered list of missing mandatory stages.\ndef missing_mandatory_stages(\n stage_status_map: dict[ComplianceStageName, CheckStageStatus],\n) -> list[ComplianceStageName]:\n return [stage for stage in MANDATORY_STAGE_ORDER if stage not in stage_status_map]\n\n\n# #endregion missing_mandatory_stages\n\n\n# #region derive_final_status [TYPE Function]\n# @BRIEF Derive final run status from stage results with deterministic blocking behavior.\n# @PRE: Stage statuses correspond to compliance checks.\n# @POST: Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes.\ndef derive_final_status(\n stage_results: Iterable[ComplianceStageRun | CheckStageResult],\n) -> CheckFinalStatus:\n status_map = stage_result_map(stage_results)\n missing = missing_mandatory_stages(status_map)\n if missing:\n return CheckFinalStatus.FAILED\n\n for stage in MANDATORY_STAGE_ORDER:\n decision = status_map.get(stage)\n if decision == CheckStageStatus.SKIPPED:\n return CheckFinalStatus.FAILED\n if decision == CheckStageStatus.FAIL:\n return CheckFinalStatus.BLOCKED\n\n return CheckFinalStatus.COMPLIANT\n\n\n# #endregion derive_final_status\n# #endregion ComplianceStages\n" + "body": "# #region ComplianceStages [C:5] [TYPE Module] [SEMANTICS clean-release, compliance, stage, package]\n# @BRIEF Define compliance stage order and helper functions for deterministic run-state evaluation.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Stage order remains deterministic for all compliance runs.\n# @SIDE_EFFECT: Registers compliance stages\n# @DATA_CONTRACT: StagePipeline -> ComplianceResult\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom typing import Dict, List\n\nfrom ....models.clean_release import (\n CheckFinalStatus,\n CheckStageResult,\n CheckStageStatus,\n ComplianceStageRun,\n)\nfrom ..enums import ComplianceDecision, ComplianceStageName\nfrom .base import ComplianceStage\nfrom .data_purity import DataPurityStage\nfrom .internal_sources_only import InternalSourcesOnlyStage\nfrom .manifest_consistency import ManifestConsistencyStage\nfrom .no_external_endpoints import NoExternalEndpointsStage\n\nMANDATORY_STAGE_ORDER: list[ComplianceStageName] = [\n ComplianceStageName.DATA_PURITY,\n ComplianceStageName.INTERNAL_SOURCES_ONLY,\n ComplianceStageName.NO_EXTERNAL_ENDPOINTS,\n ComplianceStageName.MANIFEST_CONSISTENCY,\n]\n\n\n# #region build_default_stages [TYPE Function]\n# @BRIEF Build default deterministic stage pipeline implementation order.\n# @PRE: None.\n# @POST: Returns stage instances in mandatory execution order.\ndef build_default_stages() -> list[ComplianceStage]:\n return [\n DataPurityStage(),\n InternalSourcesOnlyStage(),\n NoExternalEndpointsStage(),\n ManifestConsistencyStage(),\n ]\n\n\n# #endregion build_default_stages\n\n\n# #region stage_result_map [TYPE Function]\n# @BRIEF Convert stage result list to dictionary by stage name.\n# @PRE: stage_results may be empty or contain unique stage names.\n# @POST: Returns stage->status dictionary for downstream evaluation.\ndef stage_result_map(\n stage_results: Iterable[ComplianceStageRun | CheckStageResult],\n) -> dict[ComplianceStageName, CheckStageStatus]:\n normalized: dict[ComplianceStageName, CheckStageStatus] = {}\n for result in stage_results:\n if isinstance(result, CheckStageResult):\n normalized[ComplianceStageName(result.stage.value)] = CheckStageStatus(\n result.status.value\n )\n continue\n\n stage_name = getattr(result, \"stage_name\", None)\n decision = getattr(result, \"decision\", None)\n status = getattr(result, \"status\", None)\n\n if not stage_name:\n continue\n\n normalized_stage = ComplianceStageName(stage_name)\n if decision == ComplianceDecision.BLOCKED:\n normalized[normalized_stage] = CheckStageStatus.FAIL\n elif decision == ComplianceDecision.ERROR:\n normalized[normalized_stage] = CheckStageStatus.SKIPPED\n elif decision == ComplianceDecision.PASSED:\n normalized[normalized_stage] = CheckStageStatus.PASS\n elif decision:\n normalized[normalized_stage] = CheckStageStatus(str(decision))\n elif status:\n normalized[normalized_stage] = CheckStageStatus(str(status))\n return normalized\n\n\n# #endregion stage_result_map\n\n\n# #region missing_mandatory_stages [TYPE Function]\n# @BRIEF Identify mandatory stages that are absent from run results.\n# @PRE: stage_status_map contains zero or more known stage statuses.\n# @POST: Returns ordered list of missing mandatory stages.\ndef missing_mandatory_stages(\n stage_status_map: dict[ComplianceStageName, CheckStageStatus],\n) -> list[ComplianceStageName]:\n return [stage for stage in MANDATORY_STAGE_ORDER if stage not in stage_status_map]\n\n\n# #endregion missing_mandatory_stages\n\n\n# #region derive_final_status [TYPE Function]\n# @BRIEF Derive final run status from stage results with deterministic blocking behavior.\n# @PRE: Stage statuses correspond to compliance checks.\n# @POST: Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes.\ndef derive_final_status(\n stage_results: Iterable[ComplianceStageRun | CheckStageResult],\n) -> CheckFinalStatus:\n status_map = stage_result_map(stage_results)\n missing = missing_mandatory_stages(status_map)\n if missing:\n return CheckFinalStatus.FAILED\n\n for stage in MANDATORY_STAGE_ORDER:\n decision = status_map.get(stage)\n if decision == CheckStageStatus.SKIPPED:\n return CheckFinalStatus.FAILED\n if decision == CheckStageStatus.FAIL:\n return CheckFinalStatus.BLOCKED\n\n return CheckFinalStatus.COMPLIANT\n\n\n# #endregion derive_final_status\n# #endregion ComplianceStages\n" }, { "contract_id": "build_default_stages", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 34, - "end_line": 47, + "start_line": 36, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56193,8 +60415,8 @@ "contract_id": "stage_result_map", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 50, - "end_line": 86, + "start_line": 52, + "end_line": 88, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56231,8 +60453,8 @@ "contract_id": "missing_mandatory_stages", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 89, - "end_line": 99, + "start_line": 91, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56269,8 +60491,8 @@ "contract_id": "derive_final_status", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 102, - "end_line": 124, + "start_line": 104, + "end_line": 126, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56308,14 +60530,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/base.py", "start_line": 1, - "end_line": 132, - "tier": "TIER_2", - "complexity": 3, + "end_line": 134, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Context -> StageResult", "INVARIANT": "Stage execution is deterministic for equal input context.", "LAYER": "Domain", - "PURPOSE": "Define shared contracts and helpers for pluggable clean-release compliance stages." + "PURPOSE": "Define shared contracts and helpers for pluggable clean-release compliance stages.", + "SIDE_EFFECT": "None (deterministic execution)" }, "relations": [ { @@ -56333,25 +60557,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ComplianceStageBase [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, stage, compliance, context]\n# @BRIEF Define shared contracts and helpers for pluggable clean-release compliance stages.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Stage execution is deterministic for equal input context.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import UTC, datetime\nfrom typing import Any, Protocol\nfrom uuid import uuid4\n\nfrom ....core.logger import belief_scope, logger\nfrom ....models.clean_release import (\n CleanPolicySnapshot,\n ComplianceDecision,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom ..enums import ComplianceStageName, ViolationSeverity\n\n\n# #region ComplianceStageContext [TYPE Class]\n# @BRIEF Immutable input envelope passed to each compliance stage.\n@dataclass(frozen=True)\nclass ComplianceStageContext:\n run: ComplianceRun\n candidate: ReleaseCandidate\n manifest: DistributionManifest\n policy: CleanPolicySnapshot\n registry: SourceRegistrySnapshot\n\n\n# #endregion ComplianceStageContext\n\n\n# #region StageExecutionResult [TYPE Class]\n# @BRIEF Structured stage output containing decision, details and violations.\n@dataclass\nclass StageExecutionResult:\n decision: ComplianceDecision\n details_json: dict[str, Any] = field(default_factory=dict)\n violations: list[ComplianceViolation] = field(default_factory=list)\n\n\n# #endregion StageExecutionResult\n\n\n# #region ComplianceStage [TYPE Class]\n# @BRIEF Protocol for pluggable stage implementations.\nclass ComplianceStage(Protocol):\n stage_name: ComplianceStageName\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult: ...\n\n\n# #endregion ComplianceStage\n\n\n# #region build_stage_run_record [TYPE Function]\n# @BRIEF Build persisted stage run record from stage result.\n# @PRE: run_id and stage_name are non-empty.\n# @POST: Returns ComplianceStageRun with deterministic identifiers and timestamps.\ndef build_stage_run_record(\n *,\n run_id: str,\n stage_name: ComplianceStageName,\n result: StageExecutionResult,\n started_at: datetime | None = None,\n finished_at: datetime | None = None,\n) -> ComplianceStageRun:\n with belief_scope(\"build_stage_run_record\"):\n now = datetime.now(UTC)\n return ComplianceStageRun(\n id=f\"stg-{uuid4()}\",\n run_id=run_id,\n stage_name=stage_name.value,\n status=\"SUCCEEDED\"\n if result.decision != ComplianceDecision.ERROR\n else \"FAILED\",\n started_at=started_at or now,\n finished_at=finished_at or now,\n decision=result.decision.value,\n details_json=result.details_json,\n )\n\n\n# #endregion build_stage_run_record\n\n\n# #region build_violation [TYPE Function]\n# @BRIEF Construct a compliance violation with normalized defaults.\n# @PRE: run_id, stage_name, code and message are non-empty.\n# @POST: Returns immutable-style violation payload ready for persistence.\ndef build_violation(\n *,\n run_id: str,\n stage_name: ComplianceStageName,\n code: str,\n message: str,\n artifact_path: str | None = None,\n severity: ViolationSeverity = ViolationSeverity.MAJOR,\n evidence_json: dict[str, Any] | None = None,\n blocked_release: bool = True,\n) -> ComplianceViolation:\n with belief_scope(\"build_violation\"):\n logger.reflect(f\"Building violation stage={stage_name.value} code={code}\")\n return ComplianceViolation(\n id=f\"viol-{uuid4()}\",\n run_id=run_id,\n stage_name=stage_name.value,\n code=code,\n severity=severity.value,\n artifact_path=artifact_path,\n artifact_sha256=None,\n message=message,\n evidence_json={\n **(evidence_json or {}),\n \"blocked_release\": blocked_release,\n },\n )\n\n\n# #endregion build_violation\n\n# #endregion ComplianceStageBase\n" + "body": "# #region ComplianceStageBase [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, stage, compliance, context]\n# @BRIEF Define shared contracts and helpers for pluggable clean-release compliance stages.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Stage execution is deterministic for equal input context.\n# @SIDE_EFFECT: None (deterministic execution)\n# @DATA_CONTRACT: Context -> StageResult\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import UTC, datetime\nfrom typing import Any, Protocol\nfrom uuid import uuid4\n\nfrom ....core.logger import belief_scope, logger\nfrom ....models.clean_release import (\n CleanPolicySnapshot,\n ComplianceDecision,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom ..enums import ComplianceStageName, ViolationSeverity\n\n\n# #region ComplianceStageContext [TYPE Class]\n# @BRIEF Immutable input envelope passed to each compliance stage.\n@dataclass(frozen=True)\nclass ComplianceStageContext:\n run: ComplianceRun\n candidate: ReleaseCandidate\n manifest: DistributionManifest\n policy: CleanPolicySnapshot\n registry: SourceRegistrySnapshot\n\n\n# #endregion ComplianceStageContext\n\n\n# #region StageExecutionResult [TYPE Class]\n# @BRIEF Structured stage output containing decision, details and violations.\n@dataclass\nclass StageExecutionResult:\n decision: ComplianceDecision\n details_json: dict[str, Any] = field(default_factory=dict)\n violations: list[ComplianceViolation] = field(default_factory=list)\n\n\n# #endregion StageExecutionResult\n\n\n# #region ComplianceStage [TYPE Class]\n# @BRIEF Protocol for pluggable stage implementations.\nclass ComplianceStage(Protocol):\n stage_name: ComplianceStageName\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult: ...\n\n\n# #endregion ComplianceStage\n\n\n# #region build_stage_run_record [TYPE Function]\n# @BRIEF Build persisted stage run record from stage result.\n# @PRE: run_id and stage_name are non-empty.\n# @POST: Returns ComplianceStageRun with deterministic identifiers and timestamps.\ndef build_stage_run_record(\n *,\n run_id: str,\n stage_name: ComplianceStageName,\n result: StageExecutionResult,\n started_at: datetime | None = None,\n finished_at: datetime | None = None,\n) -> ComplianceStageRun:\n with belief_scope(\"build_stage_run_record\"):\n now = datetime.now(UTC)\n return ComplianceStageRun(\n id=f\"stg-{uuid4()}\",\n run_id=run_id,\n stage_name=stage_name.value,\n status=\"SUCCEEDED\"\n if result.decision != ComplianceDecision.ERROR\n else \"FAILED\",\n started_at=started_at or now,\n finished_at=finished_at or now,\n decision=result.decision.value,\n details_json=result.details_json,\n )\n\n\n# #endregion build_stage_run_record\n\n\n# #region build_violation [TYPE Function]\n# @BRIEF Construct a compliance violation with normalized defaults.\n# @PRE: run_id, stage_name, code and message are non-empty.\n# @POST: Returns immutable-style violation payload ready for persistence.\ndef build_violation(\n *,\n run_id: str,\n stage_name: ComplianceStageName,\n code: str,\n message: str,\n artifact_path: str | None = None,\n severity: ViolationSeverity = ViolationSeverity.MAJOR,\n evidence_json: dict[str, Any] | None = None,\n blocked_release: bool = True,\n) -> ComplianceViolation:\n with belief_scope(\"build_violation\"):\n logger.reflect(f\"Building violation stage={stage_name.value} code={code}\")\n return ComplianceViolation(\n id=f\"viol-{uuid4()}\",\n run_id=run_id,\n stage_name=stage_name.value,\n code=code,\n severity=severity.value,\n artifact_path=artifact_path,\n artifact_sha256=None,\n message=message,\n evidence_json={\n **(evidence_json or {}),\n \"blocked_release\": blocked_release,\n },\n )\n\n\n# #endregion build_violation\n\n# #endregion ComplianceStageBase\n" }, { "contract_id": "ComplianceStageContext", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 29, - "end_line": 40, + "start_line": 31, + "end_line": 42, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56367,8 +60600,8 @@ "contract_id": "StageExecutionResult", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 43, - "end_line": 52, + "start_line": 45, + "end_line": 54, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56384,8 +60617,8 @@ "contract_id": "ComplianceStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 55, - "end_line": 63, + "start_line": 57, + "end_line": 65, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56401,8 +60634,8 @@ "contract_id": "build_stage_run_record", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 66, - "end_line": 94, + "start_line": 68, + "end_line": 96, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56439,8 +60672,8 @@ "contract_id": "build_violation", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 97, - "end_line": 130, + "start_line": 99, + "end_line": 132, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56478,14 +60711,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/data_purity.py", "start_line": 1, - "end_line": 66, - "tier": "TIER_2", - "complexity": 3, + "end_line": 68, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Manifest -> DataPurityVerdict", "INVARIANT": "prohibited_detected_count > 0 always yields BLOCKED stage decision.", "LAYER": "Domain", - "PURPOSE": "Evaluate manifest purity counters and emit blocking violations for prohibited artifacts." + "PURPOSE": "Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.", + "SIDE_EFFECT": "None (read-only validation)" }, "relations": [ { @@ -56503,25 +60738,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region data_purity [C:3] [TYPE Module] [SEMANTICS clean-release, data, purity, validate, manifest]\n# @BRIEF Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: prohibited_detected_count > 0 always yields BLOCKED stage decision.\n\nfrom __future__ import annotations\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region DataPurityStage [TYPE Class]\n# @BRIEF Validate manifest summary for prohibited artifacts.\n# @PRE: context.manifest.content_json contains summary block or defaults to safe counters.\n# @POST: Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations.\nclass DataPurityStage:\n stage_name = ComplianceStageName.DATA_PURITY\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"DataPurityStage.execute\"):\n summary = context.manifest.content_json.get(\"summary\", {})\n prohibited_count = int(summary.get(\"prohibited_detected_count\", 0) or 0)\n included_count = int(summary.get(\"included_count\", 0) or 0)\n\n logger.reason(\n f\"Data purity evaluation run={context.run.id} included={included_count} prohibited={prohibited_count}\"\n )\n\n if prohibited_count <= 0:\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\n \"included_count\": included_count,\n \"prohibited_detected_count\": 0,\n },\n violations=[],\n )\n\n violation = build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"DATA_PURITY_PROHIBITED_ARTIFACTS\",\n message=f\"Detected {prohibited_count} prohibited artifact(s) in manifest snapshot\",\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"prohibited_detected_count\": prohibited_count,\n \"manifest_id\": context.manifest.id,\n },\n blocked_release=True,\n )\n return StageExecutionResult(\n decision=ComplianceDecision.BLOCKED,\n details_json={\n \"included_count\": included_count,\n \"prohibited_detected_count\": prohibited_count,\n },\n violations=[violation],\n )\n\n\n# #endregion DataPurityStage\n\n# #endregion data_purity\n" + "body": "# #region data_purity [C:5] [TYPE Module] [SEMANTICS clean-release, data, purity, validate, manifest]\n# @BRIEF Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: prohibited_detected_count > 0 always yields BLOCKED stage decision.\n# @SIDE_EFFECT: None (read-only validation)\n# @DATA_CONTRACT: Manifest -> DataPurityVerdict\n\nfrom __future__ import annotations\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region DataPurityStage [TYPE Class]\n# @BRIEF Validate manifest summary for prohibited artifacts.\n# @PRE: context.manifest.content_json contains summary block or defaults to safe counters.\n# @POST: Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations.\nclass DataPurityStage:\n stage_name = ComplianceStageName.DATA_PURITY\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"DataPurityStage.execute\"):\n summary = context.manifest.content_json.get(\"summary\", {})\n prohibited_count = int(summary.get(\"prohibited_detected_count\", 0) or 0)\n included_count = int(summary.get(\"included_count\", 0) or 0)\n\n logger.reason(\n f\"Data purity evaluation run={context.run.id} included={included_count} prohibited={prohibited_count}\"\n )\n\n if prohibited_count <= 0:\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\n \"included_count\": included_count,\n \"prohibited_detected_count\": 0,\n },\n violations=[],\n )\n\n violation = build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"DATA_PURITY_PROHIBITED_ARTIFACTS\",\n message=f\"Detected {prohibited_count} prohibited artifact(s) in manifest snapshot\",\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"prohibited_detected_count\": prohibited_count,\n \"manifest_id\": context.manifest.id,\n },\n blocked_release=True,\n )\n return StageExecutionResult(\n decision=ComplianceDecision.BLOCKED,\n details_json={\n \"included_count\": included_count,\n \"prohibited_detected_count\": prohibited_count,\n },\n violations=[violation],\n )\n\n\n# #endregion DataPurityStage\n\n# #endregion data_purity\n" }, { "contract_id": "DataPurityStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/data_purity.py", - "start_line": 15, - "end_line": 64, + "start_line": 17, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56559,14 +60803,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/internal_sources_only.py", "start_line": 1, - "end_line": 85, - "tier": "TIER_2", - "complexity": 3, + "end_line": 87, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Sources -> SourceViolationReport", "INVARIANT": "Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.", "LAYER": "Domain", - "PURPOSE": "Verify manifest-declared sources belong to trusted internal registry allowlist." + "PURPOSE": "Verify manifest-declared sources belong to trusted internal registry allowlist.", + "SIDE_EFFECT": "None (read-only validation)" }, "relations": [ { @@ -56584,25 +60830,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region internal_sources_only [C:3] [TYPE Module] [SEMANTICS clean-release, internal, source, validate]\n# @BRIEF Verify manifest-declared sources belong to trusted internal registry allowlist.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.\n\nfrom __future__ import annotations\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region InternalSourcesOnlyStage [TYPE Class]\n# @BRIEF Enforce internal-source-only policy from trusted registry snapshot.\n# @PRE: context.registry.allowed_hosts is available.\n# @POST: Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured.\nclass InternalSourcesOnlyStage:\n stage_name = ComplianceStageName.INTERNAL_SOURCES_ONLY\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"InternalSourcesOnlyStage.execute\"):\n allowed_hosts = {\n str(host).strip().lower()\n for host in (context.registry.allowed_hosts or [])\n }\n sources = context.manifest.content_json.get(\"sources\", [])\n violations = []\n\n logger.reason(\n f\"Internal sources evaluation run={context.run.id} sources={len(sources)} allowlist={len(allowed_hosts)}\"\n )\n\n for source in sources:\n host = (\n str(source.get(\"host\", \"\")).strip().lower()\n if isinstance(source, dict)\n else \"\"\n )\n if not host or host in allowed_hosts:\n continue\n\n violations.append(\n build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"SOURCE_HOST_NOT_ALLOWED\",\n message=f\"Source host '{host}' is not in trusted internal registry\",\n artifact_path=str(source.get(\"path\", \"\"))\n if isinstance(source, dict)\n else None,\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"host\": host,\n \"allowed_hosts\": sorted(allowed_hosts),\n \"manifest_id\": context.manifest.id,\n },\n blocked_release=True,\n )\n )\n\n if violations:\n return StageExecutionResult(\n decision=ComplianceDecision.BLOCKED,\n details_json={\n \"source_count\": len(sources),\n \"violations_count\": len(violations),\n },\n violations=violations,\n )\n\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\n \"source_count\": len(sources),\n \"violations_count\": 0,\n },\n violations=[],\n )\n\n\n# #endregion InternalSourcesOnlyStage\n\n# #endregion internal_sources_only\n" + "body": "# #region internal_sources_only [C:5] [TYPE Module] [SEMANTICS clean-release, internal, source, validate]\n# @BRIEF Verify manifest-declared sources belong to trusted internal registry allowlist.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.\n# @SIDE_EFFECT: None (read-only validation)\n# @DATA_CONTRACT: Sources -> SourceViolationReport\n\nfrom __future__ import annotations\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region InternalSourcesOnlyStage [TYPE Class]\n# @BRIEF Enforce internal-source-only policy from trusted registry snapshot.\n# @PRE: context.registry.allowed_hosts is available.\n# @POST: Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured.\nclass InternalSourcesOnlyStage:\n stage_name = ComplianceStageName.INTERNAL_SOURCES_ONLY\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"InternalSourcesOnlyStage.execute\"):\n allowed_hosts = {\n str(host).strip().lower()\n for host in (context.registry.allowed_hosts or [])\n }\n sources = context.manifest.content_json.get(\"sources\", [])\n violations = []\n\n logger.reason(\n f\"Internal sources evaluation run={context.run.id} sources={len(sources)} allowlist={len(allowed_hosts)}\"\n )\n\n for source in sources:\n host = (\n str(source.get(\"host\", \"\")).strip().lower()\n if isinstance(source, dict)\n else \"\"\n )\n if not host or host in allowed_hosts:\n continue\n\n violations.append(\n build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"SOURCE_HOST_NOT_ALLOWED\",\n message=f\"Source host '{host}' is not in trusted internal registry\",\n artifact_path=str(source.get(\"path\", \"\"))\n if isinstance(source, dict)\n else None,\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"host\": host,\n \"allowed_hosts\": sorted(allowed_hosts),\n \"manifest_id\": context.manifest.id,\n },\n blocked_release=True,\n )\n )\n\n if violations:\n return StageExecutionResult(\n decision=ComplianceDecision.BLOCKED,\n details_json={\n \"source_count\": len(sources),\n \"violations_count\": len(violations),\n },\n violations=violations,\n )\n\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\n \"source_count\": len(sources),\n \"violations_count\": 0,\n },\n violations=[],\n )\n\n\n# #endregion InternalSourcesOnlyStage\n\n# #endregion internal_sources_only\n" }, { "contract_id": "InternalSourcesOnlyStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/internal_sources_only.py", - "start_line": 15, - "end_line": 83, + "start_line": 17, + "end_line": 85, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56640,14 +60895,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/manifest_consistency.py", "start_line": 1, - "end_line": 70, - "tier": "TIER_2", - "complexity": 3, + "end_line": 72, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "RunData -> ConsistencyVerdict", "INVARIANT": "Digest mismatch between run and manifest yields ERROR with blocking violation evidence.", "LAYER": "Domain", - "PURPOSE": "Ensure run is bound to the exact manifest snapshot and digest used at run creation time." + "PURPOSE": "Ensure run is bound to the exact manifest snapshot and digest used at run creation time.", + "SIDE_EFFECT": "None (read-only validation)" }, "relations": [ { @@ -56665,25 +60922,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region manifest_consistency [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, consistency, validate]\n# @BRIEF Ensure run is bound to the exact manifest snapshot and digest used at run creation time.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Digest mismatch between run and manifest yields ERROR with blocking violation evidence.\n\nfrom __future__ import annotations\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region ManifestConsistencyStage [TYPE Class]\n# @BRIEF Validate run/manifest linkage consistency.\n# @PRE: context.run and context.manifest are loaded from repository for same run.\n# @POST: Returns PASSED when digests match, otherwise ERROR with one violation.\nclass ManifestConsistencyStage:\n stage_name = ComplianceStageName.MANIFEST_CONSISTENCY\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"ManifestConsistencyStage.execute\"):\n expected_digest = str(context.run.manifest_digest or \"\").strip()\n actual_digest = str(context.manifest.manifest_digest or \"\").strip()\n\n logger.reason(\n f\"Manifest consistency evaluation run={context.run.id} manifest={context.manifest.id} \"\n f\"expected_digest={expected_digest} actual_digest={actual_digest}\"\n )\n\n if expected_digest and expected_digest == actual_digest:\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\n \"manifest_id\": context.manifest.id,\n \"manifest_digest\": actual_digest,\n \"consistent\": True,\n },\n violations=[],\n )\n\n violation = build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"MANIFEST_DIGEST_MISMATCH\",\n message=\"Run manifest digest does not match resolved manifest snapshot\",\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"manifest_id\": context.manifest.id,\n \"run_manifest_digest\": expected_digest,\n \"actual_manifest_digest\": actual_digest,\n },\n blocked_release=True,\n )\n return StageExecutionResult(\n decision=ComplianceDecision.ERROR,\n details_json={\n \"manifest_id\": context.manifest.id,\n \"run_manifest_digest\": expected_digest,\n \"actual_manifest_digest\": actual_digest,\n \"consistent\": False,\n },\n violations=[violation],\n )\n\n\n# #endregion ManifestConsistencyStage\n\n# #endregion manifest_consistency\n" + "body": "# #region manifest_consistency [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, consistency, validate]\n# @BRIEF Ensure run is bound to the exact manifest snapshot and digest used at run creation time.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Digest mismatch between run and manifest yields ERROR with blocking violation evidence.\n# @SIDE_EFFECT: None (read-only validation)\n# @DATA_CONTRACT: RunData -> ConsistencyVerdict\n\nfrom __future__ import annotations\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region ManifestConsistencyStage [TYPE Class]\n# @BRIEF Validate run/manifest linkage consistency.\n# @PRE: context.run and context.manifest are loaded from repository for same run.\n# @POST: Returns PASSED when digests match, otherwise ERROR with one violation.\nclass ManifestConsistencyStage:\n stage_name = ComplianceStageName.MANIFEST_CONSISTENCY\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"ManifestConsistencyStage.execute\"):\n expected_digest = str(context.run.manifest_digest or \"\").strip()\n actual_digest = str(context.manifest.manifest_digest or \"\").strip()\n\n logger.reason(\n f\"Manifest consistency evaluation run={context.run.id} manifest={context.manifest.id} \"\n f\"expected_digest={expected_digest} actual_digest={actual_digest}\"\n )\n\n if expected_digest and expected_digest == actual_digest:\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\n \"manifest_id\": context.manifest.id,\n \"manifest_digest\": actual_digest,\n \"consistent\": True,\n },\n violations=[],\n )\n\n violation = build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"MANIFEST_DIGEST_MISMATCH\",\n message=\"Run manifest digest does not match resolved manifest snapshot\",\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"manifest_id\": context.manifest.id,\n \"run_manifest_digest\": expected_digest,\n \"actual_manifest_digest\": actual_digest,\n },\n blocked_release=True,\n )\n return StageExecutionResult(\n decision=ComplianceDecision.ERROR,\n details_json={\n \"manifest_id\": context.manifest.id,\n \"run_manifest_digest\": expected_digest,\n \"actual_manifest_digest\": actual_digest,\n \"consistent\": False,\n },\n violations=[violation],\n )\n\n\n# #endregion ManifestConsistencyStage\n\n# #endregion manifest_consistency\n" }, { "contract_id": "ManifestConsistencyStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/manifest_consistency.py", - "start_line": 15, - "end_line": 68, + "start_line": 17, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56721,14 +60987,16 @@ "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/no_external_endpoints.py", "start_line": 1, - "end_line": 91, - "tier": "TIER_2", - "complexity": 3, + "end_line": 93, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "Endpoints -> EndpointViolationReport", "INVARIANT": "Endpoint outside allowed scheme/host always yields BLOCKED stage decision.", "LAYER": "Domain", - "PURPOSE": "Block manifest payloads that expose external endpoints outside trusted schemes and hosts." + "PURPOSE": "Block manifest payloads that expose external endpoints outside trusted schemes and hosts.", + "SIDE_EFFECT": "None (read-only validation)" }, "relations": [ { @@ -56746,25 +61014,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region no_external_endpoints [C:3] [TYPE Module] [SEMANTICS clean-release, endpoint, validate, manifest, compliance]\n# @BRIEF Block manifest payloads that expose external endpoints outside trusted schemes and hosts.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Endpoint outside allowed scheme/host always yields BLOCKED stage decision.\n\nfrom __future__ import annotations\n\nfrom urllib.parse import urlparse\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region NoExternalEndpointsStage [TYPE Class]\n# @BRIEF Validate endpoint references from manifest against trusted registry.\n# @PRE: context.registry includes allowed hosts and schemes.\n# @POST: Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations.\nclass NoExternalEndpointsStage:\n stage_name = ComplianceStageName.NO_EXTERNAL_ENDPOINTS\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"NoExternalEndpointsStage.execute\"):\n endpoints = context.manifest.content_json.get(\"endpoints\", [])\n allowed_hosts = {\n str(host).strip().lower()\n for host in (context.registry.allowed_hosts or [])\n }\n allowed_schemes = {\n str(scheme).strip().lower()\n for scheme in (context.registry.allowed_schemes or [])\n }\n violations = []\n\n logger.reason(\n f\"Endpoint isolation evaluation run={context.run.id} endpoints={len(endpoints)} \"\n f\"allowed_hosts={len(allowed_hosts)} allowed_schemes={len(allowed_schemes)}\"\n )\n\n for endpoint in endpoints:\n raw = str(endpoint).strip()\n if not raw:\n continue\n parsed = urlparse(raw)\n host = (parsed.hostname or \"\").lower()\n scheme = (parsed.scheme or \"\").lower()\n\n if host in allowed_hosts and scheme in allowed_schemes:\n continue\n\n violations.append(\n build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"EXTERNAL_ENDPOINT_DETECTED\",\n message=f\"Endpoint '{raw}' is outside trusted internal network boundary\",\n artifact_path=None,\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"endpoint\": raw,\n \"host\": host,\n \"scheme\": scheme,\n \"allowed_hosts\": sorted(allowed_hosts),\n \"allowed_schemes\": sorted(allowed_schemes),\n },\n blocked_release=True,\n )\n )\n\n if violations:\n return StageExecutionResult(\n decision=ComplianceDecision.BLOCKED,\n details_json={\n \"endpoint_count\": len(endpoints),\n \"violations_count\": len(violations),\n },\n violations=violations,\n )\n\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\"endpoint_count\": len(endpoints), \"violations_count\": 0},\n violations=[],\n )\n\n\n# #endregion NoExternalEndpointsStage\n\n# #endregion no_external_endpoints\n" + "body": "# #region no_external_endpoints [C:5] [TYPE Module] [SEMANTICS clean-release, endpoint, validate, manifest, compliance]\n# @BRIEF Block manifest payloads that expose external endpoints outside trusted schemes and hosts.\n# @LAYER: Domain\n# @RELATION IMPLEMENTS -> [ComplianceStage]\n# @RELATION DEPENDS_ON -> [ComplianceStageBase]\n# @INVARIANT: Endpoint outside allowed scheme/host always yields BLOCKED stage decision.\n# @SIDE_EFFECT: None (read-only validation)\n# @DATA_CONTRACT: Endpoints -> EndpointViolationReport\n\nfrom __future__ import annotations\n\nfrom urllib.parse import urlparse\n\nfrom ....core.logger import belief_scope, logger\nfrom ..enums import ComplianceDecision, ComplianceStageName, ViolationSeverity\nfrom .base import ComplianceStageContext, StageExecutionResult, build_violation\n\n\n# #region NoExternalEndpointsStage [TYPE Class]\n# @BRIEF Validate endpoint references from manifest against trusted registry.\n# @PRE: context.registry includes allowed hosts and schemes.\n# @POST: Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations.\nclass NoExternalEndpointsStage:\n stage_name = ComplianceStageName.NO_EXTERNAL_ENDPOINTS\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult:\n with belief_scope(\"NoExternalEndpointsStage.execute\"):\n endpoints = context.manifest.content_json.get(\"endpoints\", [])\n allowed_hosts = {\n str(host).strip().lower()\n for host in (context.registry.allowed_hosts or [])\n }\n allowed_schemes = {\n str(scheme).strip().lower()\n for scheme in (context.registry.allowed_schemes or [])\n }\n violations = []\n\n logger.reason(\n f\"Endpoint isolation evaluation run={context.run.id} endpoints={len(endpoints)} \"\n f\"allowed_hosts={len(allowed_hosts)} allowed_schemes={len(allowed_schemes)}\"\n )\n\n for endpoint in endpoints:\n raw = str(endpoint).strip()\n if not raw:\n continue\n parsed = urlparse(raw)\n host = (parsed.hostname or \"\").lower()\n scheme = (parsed.scheme or \"\").lower()\n\n if host in allowed_hosts and scheme in allowed_schemes:\n continue\n\n violations.append(\n build_violation(\n run_id=context.run.id,\n stage_name=self.stage_name,\n code=\"EXTERNAL_ENDPOINT_DETECTED\",\n message=f\"Endpoint '{raw}' is outside trusted internal network boundary\",\n artifact_path=None,\n severity=ViolationSeverity.CRITICAL,\n evidence_json={\n \"endpoint\": raw,\n \"host\": host,\n \"scheme\": scheme,\n \"allowed_hosts\": sorted(allowed_hosts),\n \"allowed_schemes\": sorted(allowed_schemes),\n },\n blocked_release=True,\n )\n )\n\n if violations:\n return StageExecutionResult(\n decision=ComplianceDecision.BLOCKED,\n details_json={\n \"endpoint_count\": len(endpoints),\n \"violations_count\": len(violations),\n },\n violations=violations,\n )\n\n return StageExecutionResult(\n decision=ComplianceDecision.PASSED,\n details_json={\"endpoint_count\": len(endpoints), \"violations_count\": 0},\n violations=[],\n )\n\n\n# #endregion NoExternalEndpointsStage\n\n# #endregion no_external_endpoints\n" }, { "contract_id": "NoExternalEndpointsStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/no_external_endpoints.py", - "start_line": 17, - "end_line": 89, + "start_line": 19, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -58306,7 +62583,7 @@ "contract_type": "Module", "file_path": "backend/src/services/git/_base.py", "start_line": 1, - "end_line": 383, + "end_line": 394, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -58406,20 +62683,20 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceBase [C:4] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin, lock, http]\n# @LAYER: Infra\n# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, and shared HTTP client pool.\n# @RELATION INHERITED_BY -> [GitService]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [GitRepository]\n\nimport os\nimport re\nimport shutil\nimport threading\nfrom contextlib import contextmanager\nfrom pathlib import Path\n\nimport httpx\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import InvalidGitRepositoryError, NoSuchPathError\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.config import AppConfigRecord\nfrom src.models.git import GitRepository\n\n\n# #region GitServiceBase [C:4] [TYPE Class]\n# @BRIEF Base class for GitService providing initialization, path resolution, repository lifecycle, identity, concurrent locking, and shared HTTP client.\n# @PRE base_path is a valid string path.\n# @POST GitService is initialized; base_path directory exists; shared AsyncClient ready; lock registry empty.\n# @SIDE_EFFECT Creates HTTP connection pool (max_keepalive=20, max_connections=100).\nclass GitServiceBase:\n # region GitService_init [C:4] [TYPE Function]\n # @PURPOSE: Initializes the GitService with a base path, concurrent access locks, and shared HTTP client.\n # @PRE: base_path is a valid string path.\n # @POST: GitService is initialized; base_path directory exists; _lock_registry, _http_client ready.\n def __init__(self, base_path: str = \"git_repos\"):\n with belief_scope(\"GitService.__init__\"):\n backend_root = Path(__file__).parents[3]\n self.legacy_base_path = str((backend_root / \"git_repos\").resolve())\n self._uses_default_base_path = base_path == \"git_repos\"\n self.base_path = self._resolve_base_path(base_path)\n self._ensure_base_path_exists()\n\n # Fix 3: Per-dashboard reentrant lock registry for concurrent access protection\n self._lock_registry: dict[int, threading.RLock] = {}\n self._reg_lock = threading.Lock()\n\n # Fix 5: Shared httpx.AsyncClient with connection pooling\n self._http_client = httpx.AsyncClient(\n timeout=30.0,\n limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),\n )\n # endregion GitService_init\n\n # region _get_lock [C:2] [TYPE Function] [SEMANTICS lock,concurrency]\n # @BRIEF Get or create a per-dashboard reentrant lock for thread-safe GitPython access.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Returns a threading.RLock unique to the given dashboard_id.\n def _get_lock(self, dashboard_id: int) -> threading.RLock:\n with self._reg_lock:\n if dashboard_id not in self._lock_registry:\n self._lock_registry[dashboard_id] = threading.RLock()\n return self._lock_registry[dashboard_id]\n # endregion _get_lock\n\n # region _locked [C:2] [TYPE Function] [SEMANTICS lock,context,concurrency]\n # @BRIEF Context manager that acquires the per-dashboard reentrant lock for the duration of the block.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Lock is acquired on enter, released on exit.\n @contextmanager\n def _locked(self, dashboard_id: int):\n lock = self._get_lock(dashboard_id)\n lock.acquire()\n try:\n yield\n finally:\n lock.release()\n # endregion _locked\n\n # region _clone_with_auth [C:2] [TYPE Function] [SEMANTICS git,clone,auth,security]\n # @BRIEF Clone repository with PAT and immediately strip credentials from origin remote URL.\n # @PRE: remote_url is a valid Git URL; pat is provided when required; repo_path is writable.\n # @POST: Repo cloned at repo_path; origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository to local filesystem.\n def _clone_with_auth(self, remote_url: str, repo_path: str, pat: str) -> Repo:\n auth_url = remote_url\n if pat and \"://\" in remote_url:\n proto, rest = remote_url.split(\"://\", 1)\n auth_url = f\"{proto}://oauth2:{pat}@{rest}\"\n repo = Repo.clone_from(auth_url, repo_path)\n # Strip PAT from origin URL to prevent credential leakage in .git/config, logs, ps aux\n repo.git.remote(\"set-url\", \"origin\", remote_url)\n return repo\n # endregion _clone_with_auth\n\n # region _ensure_base_path_exists [TYPE Function]\n # @PURPOSE: Ensure the repositories root directory exists and is a directory.\n # @PRE: self.base_path is resolved to filesystem path.\n # @POST: self.base_path exists as directory or raises ValueError.\n def _ensure_base_path_exists(self) -> None:\n base = Path(self.base_path)\n if base.exists() and not base.is_dir():\n raise ValueError(f\"Git repositories base path is not a directory: {self.base_path}\")\n try:\n base.mkdir(parents=True, exist_ok=True)\n except (PermissionError, OSError) as e:\n logger.warning(\n f\"[_ensure_base_path_exists][Coherence:Failed] Cannot create Git repositories base path: {self.base_path}. Error: {e}\"\n )\n raise ValueError(f\"Cannot create Git repositories base path: {self.base_path}. {e}\")\n # endregion _ensure_base_path_exists\n\n # region _resolve_base_path [TYPE Function]\n # @PURPOSE: Resolve base repository directory from explicit argument or global storage settings.\n # @PRE: base_path is a string path.\n # @POST: Returns absolute path for Git repositories root.\n # @RETURN: str\n def _resolve_base_path(self, base_path: str) -> str:\n backend_root = Path(__file__).parents[3]\n fallback_path = str((backend_root / base_path).resolve())\n if base_path != \"git_repos\":\n return fallback_path\n try:\n session = SessionLocal()\n try:\n config_row = session.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n finally:\n session.close()\n payload = (config_row.payload if config_row and config_row.payload else {}) if config_row else {}\n storage_cfg = payload.get(\"settings\", {}).get(\"storage\", {}) if isinstance(payload, dict) else {}\n root_path = str(storage_cfg.get(\"root_path\", \"\")).strip()\n repo_path = str(storage_cfg.get(\"repo_path\", \"\")).strip()\n if not root_path:\n return fallback_path\n project_root = Path(__file__).parents[4]\n root = Path(root_path)\n if not root.is_absolute():\n root = (project_root / root).resolve()\n repo_root = Path(repo_path) if repo_path else Path(\"repositorys\")\n if repo_root.is_absolute():\n return str(repo_root.resolve())\n return str((root / repo_root).resolve())\n except Exception as e:\n logger.warning(f\"[_resolve_base_path][Coherence:Failed] Falling back to default path: {e}\")\n return fallback_path\n # endregion _resolve_base_path\n\n # region _normalize_repo_key [TYPE Function]\n # @PURPOSE: Convert user/dashboard-provided key to safe filesystem directory name.\n # @PRE: repo_key can be None/empty.\n # @POST: Returns normalized non-empty key.\n # @RETURN: str\n def _normalize_repo_key(self, repo_key: str | None) -> str:\n raw_key = str(repo_key or \"\").strip().lower()\n normalized = re.sub(r\"[^a-z0-9._-]+\", \"-\", raw_key).strip(\"._-\")\n return normalized or \"dashboard\"\n # endregion _normalize_repo_key\n\n # region _update_repo_local_path [TYPE Function]\n # @PURPOSE: Persist repository local_path in GitRepository table when record exists.\n # @PRE: dashboard_id is valid integer.\n # @POST: local_path is updated for existing record.\n def _update_repo_local_path(self, dashboard_id: int, local_path: str) -> None:\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n db_repo.local_path = local_path\n session.commit()\n finally:\n session.close()\n except Exception as e:\n logger.warning(f\"[_update_repo_local_path][Coherence:Failed] {e}\")\n # endregion _update_repo_local_path\n\n # region _migrate_repo_directory [TYPE Function]\n # @PURPOSE: Move legacy repository directory to target path and sync DB metadata.\n # @PRE: source_path exists.\n # @POST: Repository content available at target_path.\n # @RETURN: str\n def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str:\n source_abs = os.path.abspath(source_path)\n target_abs = os.path.abspath(target_path)\n if source_abs == target_abs:\n return source_abs\n if os.path.exists(target_abs):\n logger.reason(f\"Target already exists, keeping source path: {target_abs}\", extra={\"src\": \"_migrate_repo_directory\"})\n return source_abs\n Path(target_abs).parent.mkdir(parents=True, exist_ok=True)\n try:\n os.replace(source_abs, target_abs)\n except OSError:\n shutil.move(source_abs, target_abs)\n self._update_repo_local_path(dashboard_id, target_abs)\n logger.info(\n f\"[_migrate_repo_directory][Coherence:OK] Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}\"\n )\n return target_abs\n # endregion _migrate_repo_directory\n\n # region _get_repo_path [TYPE Function]\n # @PURPOSE: Resolves the local filesystem path for a dashboard's repository.\n # @PARAM: dashboard_id (int)\n # @PARAM: repo_key (Optional[str]) - Slug-like key used when DB local_path is absent.\n # @PRE: dashboard_id is an integer.\n # @POST: Returns DB-local_path when present, otherwise base_path/.\n # @RETURN: str\n def _get_repo_path(self, dashboard_id: int, repo_key: str | None = None) -> str:\n with belief_scope(\"GitService._get_repo_path\"):\n if dashboard_id is None:\n raise ValueError(\"dashboard_id cannot be None\")\n self._ensure_base_path_exists()\n fallback_key = repo_key if repo_key is not None else str(dashboard_id)\n normalized_key = self._normalize_repo_key(fallback_key)\n target_path = os.path.join(self.base_path, normalized_key)\n if not self._uses_default_base_path:\n return target_path\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n finally:\n session.close()\n if db_repo and db_repo.local_path:\n db_path = os.path.abspath(db_repo.local_path)\n if os.path.exists(db_path):\n if (\n os.path.abspath(self.base_path) != os.path.abspath(self.legacy_base_path)\n and db_path.startswith(os.path.abspath(self.legacy_base_path) + os.sep)\n ):\n return self._migrate_repo_directory(dashboard_id, db_path, target_path)\n return db_path\n except Exception as e:\n logger.warning(f\"[_get_repo_path][Coherence:Failed] Could not resolve local_path from DB: {e}\")\n legacy_id_path = os.path.join(self.legacy_base_path, str(dashboard_id))\n if os.path.exists(legacy_id_path) and not os.path.exists(target_path):\n return self._migrate_repo_directory(dashboard_id, legacy_id_path, target_path)\n if os.path.exists(target_path):\n self._update_repo_local_path(dashboard_id, target_path)\n return target_path\n # endregion _get_repo_path\n\n # region init_repo [C:4] [TYPE Function] [SEMANTICS git,clone,init,lock]\n # @PURPOSE: Initialize or clone a repository for a dashboard with concurrent access protection.\n # @PARAM: dashboard_id (int), remote_url (str), pat (str), repo_key (Optional[str]).\n # @PRE: dashboard_id is int, remote_url is valid Git URL, pat is provided.\n # @POST: Repository is cloned or opened at the local path. Origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository; modifies local filesystem; protected by per-dashboard lock.\n # @RETURN: Repo\n def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None) -> Repo:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.init_repo\"):\n self._ensure_base_path_exists()\n repo_path = self._get_repo_path(dashboard_id, repo_key=repo_key or str(dashboard_id))\n Path(repo_path).parent.mkdir(parents=True, exist_ok=True)\n if os.path.exists(repo_path):\n logger.reason(f\"Opening existing repo at {repo_path}\", extra={\"src\": \"init_repo\"})\n try:\n repo = Repo(repo_path)\n except (InvalidGitRepositoryError, NoSuchPathError):\n logger.reason(f\"Existing path is not a Git repository, recreating: {repo_path}\", extra={\"src\": \"init_repo\"})\n stale_path = Path(repo_path)\n if stale_path.exists():\n shutil.rmtree(stale_path, ignore_errors=True)\n if stale_path.exists():\n try:\n stale_path.unlink()\n except Exception:\n pass\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n logger.reason(f\"Cloning {remote_url} to {repo_path}\", extra={\"src\": \"init_repo\"})\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n # endregion init_repo\n\n # region delete_repo [C:4] [TYPE Function] [SEMANTICS git,delete,lock]\n # @PURPOSE: Remove local repository and DB binding for a dashboard with concurrent access protection.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Local path is deleted when present and GitRepository row is removed.\n # @SIDE_EFFECT Deletes filesystem directory and DB record; protected by per-dashboard lock.\n def delete_repo(self, dashboard_id: int) -> None:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.delete_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n removed_files = False\n if os.path.exists(repo_path):\n if os.path.isdir(repo_path):\n shutil.rmtree(repo_path)\n else:\n os.remove(repo_path)\n removed_files = True\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n session.delete(db_repo)\n session.commit()\n return\n if removed_files:\n return\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_id} not found\",\n )\n except HTTPException:\n session.rollback()\n raise\n except Exception as e:\n session.rollback()\n logger.error(f\"[delete_repo][Coherence:Failed] Failed to delete repository for dashboard {dashboard_id}: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to delete repository: {e!s}\")\n finally:\n session.close()\n # endregion delete_repo\n\n # region get_repo [C:4] [TYPE Function] [SEMANTICS git,open,lock]\n # @PURPOSE: Get Repo object for a dashboard with concurrent access protection.\n # @PRE: Repository must exist on disk for the given dashboard_id.\n # @POST: Returns a GitPython Repo instance for the dashboard.\n # @RETURN: Repo\n def get_repo(self, dashboard_id: int) -> Repo:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n logger.error(f\"[get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist\")\n raise HTTPException(status_code=404, detail=f\"Repository for dashboard {dashboard_id} not found\")\n try:\n return Repo(repo_path)\n except Exception as e:\n logger.error(f\"[get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}\")\n raise HTTPException(status_code=500, detail=\"Failed to open local Git repository\")\n # endregion get_repo\n\n # region configure_identity [C:4] [TYPE Function] [SEMANTICS git,identity,lock]\n # @PURPOSE: Configure repository-local Git committer identity with concurrent access protection.\n # @PRE: dashboard_id repository exists; git_username/git_email may be empty.\n # @POST: Repository config has user.name and user.email when both identity values are provided.\n # @SIDE_EFFECT Writes to repository git config; protected by per-dashboard lock.\n def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.configure_identity\"):\n normalized_username = str(git_username or \"\").strip()\n normalized_email = str(git_email or \"\").strip()\n if not normalized_username or not normalized_email:\n return\n repo = self.get_repo(dashboard_id)\n try:\n with repo.config_writer(config_level=\"repository\") as config_writer:\n config_writer.set_value(\"user\", \"name\", normalized_username)\n config_writer.set_value(\"user\", \"email\", normalized_email)\n logger.reason(f\"Applied repository-local git identity for dashboard {dashboard_id}\", extra={\"src\": \"configure_identity\"})\n except Exception as e:\n logger.error(f\"[configure_identity][Coherence:Failed] Failed to configure git identity: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to configure git identity: {e!s}\")\n # endregion configure_identity\n\n # region close [C:2] [TYPE Function] [SEMANTICS http,cleanup,shutdown]\n # @BRIEF Gracefully close the shared HTTP client (connection pool).\n # @PRE: _http_client is initialized.\n # @POST: HTTP connections closed.\n async def close(self):\n await self._http_client.aclose()\n # endregion close\n# #endregion GitServiceBase\n# #endregion GitServiceBase\n" + "body": "# #region GitServiceBase [C:4] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin, lock, http]\n# @LAYER: Infra\n# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, and shared HTTP client pool.\n# @RELATION INHERITED_BY -> [GitService]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [GitRepository]\n\nimport os\nimport re\nimport shutil\nimport threading\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom urllib.parse import quote\n\nimport httpx\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import InvalidGitRepositoryError, NoSuchPathError\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.config import AppConfigRecord\nfrom src.models.git import GitRepository\n\n\n# #region GitServiceBase [C:4] [TYPE Class]\n# @BRIEF Base class for GitService providing initialization, path resolution, repository lifecycle, identity, concurrent locking, and shared HTTP client.\n# @PRE base_path is a valid string path.\n# @POST GitService is initialized; base_path directory exists; shared AsyncClient ready; lock registry empty.\n# @SIDE_EFFECT Creates HTTP connection pool (max_keepalive=20, max_connections=100).\nclass GitServiceBase:\n # region GitService_init [C:4] [TYPE Function]\n # @PURPOSE: Initializes the GitService with a base path, concurrent access locks, and shared HTTP client.\n # @PRE: base_path is a valid string path.\n # @POST: GitService is initialized; base_path directory exists; _lock_registry, _http_client ready.\n def __init__(self, base_path: str = \"git_repos\"):\n with belief_scope(\"GitService.__init__\"):\n backend_root = Path(__file__).parents[3]\n self.legacy_base_path = str((backend_root / \"git_repos\").resolve())\n self._uses_default_base_path = base_path == \"git_repos\"\n self.base_path = self._resolve_base_path(base_path)\n self._ensure_base_path_exists()\n\n # Fix 3: Per-dashboard reentrant lock registry for concurrent access protection\n self._lock_registry: dict[int, threading.RLock] = {}\n self._reg_lock = threading.Lock()\n\n # Fix: close() race condition protection\n self._closed = False\n self._close_lock = threading.Lock()\n\n # Fix 5: Shared httpx.AsyncClient with connection pooling\n self._http_client = httpx.AsyncClient(\n timeout=30.0,\n limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),\n )\n # endregion GitService_init\n\n # region _get_lock [C:2] [TYPE Function] [SEMANTICS lock,concurrency]\n # @BRIEF Get or create a per-dashboard reentrant lock for thread-safe GitPython access.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Returns a threading.RLock unique to the given dashboard_id.\n def _get_lock(self, dashboard_id: int) -> threading.RLock:\n with self._reg_lock:\n if dashboard_id not in self._lock_registry:\n self._lock_registry[dashboard_id] = threading.RLock()\n return self._lock_registry[dashboard_id]\n # endregion _get_lock\n\n # region _locked [C:2] [TYPE Function] [SEMANTICS lock,context,concurrency]\n # @BRIEF Context manager that acquires the per-dashboard reentrant lock for the duration of the block.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Lock is acquired on enter, released on exit.\n @contextmanager\n def _locked(self, dashboard_id: int):\n if self._closed:\n raise RuntimeError(\"GitService is closed\")\n lock = self._get_lock(dashboard_id)\n lock.acquire()\n try:\n yield\n finally:\n lock.release()\n # endregion _locked\n\n # region _clone_with_auth [C:2] [TYPE Function] [SEMANTICS git,clone,auth,security]\n # @BRIEF Clone repository with PAT and immediately strip credentials from origin remote URL.\n # @PRE: remote_url is a valid Git URL; pat is provided when required; repo_path is writable.\n # @POST: Repo cloned at repo_path; origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository to local filesystem.\n def _clone_with_auth(self, remote_url: str, repo_path: str, pat: str) -> Repo:\n auth_url = remote_url\n if pat and \"://\" in remote_url:\n proto, rest = remote_url.split(\"://\", 1)\n auth_url = f\"{proto}://oauth2:{quote(pat, safe='')}@{rest}\"\n repo = Repo.clone_from(auth_url, repo_path)\n # Strip PAT from origin URL to prevent credential leakage in .git/config, logs, ps aux\n repo.git.remote(\"set-url\", \"origin\", remote_url)\n return repo\n # endregion _clone_with_auth\n\n # region _ensure_base_path_exists [TYPE Function]\n # @PURPOSE: Ensure the repositories root directory exists and is a directory.\n # @PRE: self.base_path is resolved to filesystem path.\n # @POST: self.base_path exists as directory or raises ValueError.\n def _ensure_base_path_exists(self) -> None:\n base = Path(self.base_path)\n if base.exists() and not base.is_dir():\n raise ValueError(f\"Git repositories base path is not a directory: {self.base_path}\")\n try:\n base.mkdir(parents=True, exist_ok=True)\n except (PermissionError, OSError) as e:\n logger.warning(\n f\"[_ensure_base_path_exists][Coherence:Failed] Cannot create Git repositories base path: {self.base_path}. Error: {e}\"\n )\n raise ValueError(f\"Cannot create Git repositories base path: {self.base_path}. {e}\")\n # endregion _ensure_base_path_exists\n\n # region _resolve_base_path [TYPE Function]\n # @PURPOSE: Resolve base repository directory from explicit argument or global storage settings.\n # @PRE: base_path is a string path.\n # @POST: Returns absolute path for Git repositories root.\n # @RETURN: str\n def _resolve_base_path(self, base_path: str) -> str:\n backend_root = Path(__file__).parents[3]\n fallback_path = str((backend_root / base_path).resolve())\n if base_path != \"git_repos\":\n return fallback_path\n try:\n session = SessionLocal()\n try:\n config_row = session.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n finally:\n session.close()\n payload = (config_row.payload if config_row and config_row.payload else {}) if config_row else {}\n storage_cfg = payload.get(\"settings\", {}).get(\"storage\", {}) if isinstance(payload, dict) else {}\n root_path = str(storage_cfg.get(\"root_path\", \"\")).strip()\n repo_path = str(storage_cfg.get(\"repo_path\", \"\")).strip()\n if not root_path:\n return fallback_path\n project_root = Path(__file__).parents[4]\n root = Path(root_path)\n if not root.is_absolute():\n root = (project_root / root).resolve()\n repo_root = Path(repo_path) if repo_path else Path(\"repositorys\")\n if repo_root.is_absolute():\n return str(repo_root.resolve())\n return str((root / repo_root).resolve())\n except Exception as e:\n logger.warning(f\"[_resolve_base_path][Coherence:Failed] Falling back to default path: {e}\")\n return fallback_path\n # endregion _resolve_base_path\n\n # region _normalize_repo_key [TYPE Function]\n # @PURPOSE: Convert user/dashboard-provided key to safe filesystem directory name.\n # @PRE: repo_key can be None/empty.\n # @POST: Returns normalized non-empty key.\n # @RETURN: str\n def _normalize_repo_key(self, repo_key: str | None) -> str:\n raw_key = str(repo_key or \"\").strip().lower()\n normalized = re.sub(r\"[^a-z0-9._-]+\", \"-\", raw_key).strip(\"._-\")\n return normalized or \"dashboard\"\n # endregion _normalize_repo_key\n\n # region _update_repo_local_path [TYPE Function]\n # @PURPOSE: Persist repository local_path in GitRepository table when record exists.\n # @PRE: dashboard_id is valid integer.\n # @POST: local_path is updated for existing record.\n def _update_repo_local_path(self, dashboard_id: int, local_path: str) -> None:\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n db_repo.local_path = local_path\n session.commit()\n finally:\n session.close()\n except Exception as e:\n logger.warning(f\"[_update_repo_local_path][Coherence:Failed] {e}\")\n # endregion _update_repo_local_path\n\n # region _migrate_repo_directory [TYPE Function]\n # @PURPOSE: Move legacy repository directory to target path and sync DB metadata.\n # @PRE: source_path exists.\n # @POST: Repository content available at target_path.\n # @RETURN: str\n def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str:\n source_abs = os.path.abspath(source_path)\n target_abs = os.path.abspath(target_path)\n if source_abs == target_abs:\n return source_abs\n if os.path.exists(target_abs):\n logger.reason(f\"Target already exists, keeping source path: {target_abs}\", extra={\"src\": \"_migrate_repo_directory\"})\n return source_abs\n Path(target_abs).parent.mkdir(parents=True, exist_ok=True)\n try:\n os.replace(source_abs, target_abs)\n except OSError:\n shutil.move(source_abs, target_abs)\n self._update_repo_local_path(dashboard_id, target_abs)\n logger.info(\n f\"[_migrate_repo_directory][Coherence:OK] Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}\"\n )\n return target_abs\n # endregion _migrate_repo_directory\n\n # region _get_repo_path [TYPE Function]\n # @PURPOSE: Resolves the local filesystem path for a dashboard's repository.\n # @PARAM: dashboard_id (int)\n # @PARAM: repo_key (Optional[str]) - Slug-like key used when DB local_path is absent.\n # @PRE: dashboard_id is an integer.\n # @POST: Returns DB-local_path when present, otherwise base_path/.\n # @RETURN: str\n def _get_repo_path(self, dashboard_id: int, repo_key: str | None = None) -> str:\n with belief_scope(\"GitService._get_repo_path\"):\n if dashboard_id is None:\n raise ValueError(\"dashboard_id cannot be None\")\n self._ensure_base_path_exists()\n fallback_key = repo_key if repo_key is not None else str(dashboard_id)\n normalized_key = self._normalize_repo_key(fallback_key)\n target_path = os.path.join(self.base_path, normalized_key)\n if not self._uses_default_base_path:\n return target_path\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n finally:\n session.close()\n if db_repo and db_repo.local_path:\n db_path = os.path.abspath(db_repo.local_path)\n if os.path.exists(db_path):\n if (\n os.path.abspath(self.base_path) != os.path.abspath(self.legacy_base_path)\n and db_path.startswith(os.path.abspath(self.legacy_base_path) + os.sep)\n ):\n return self._migrate_repo_directory(dashboard_id, db_path, target_path)\n return db_path\n except Exception as e:\n logger.warning(f\"[_get_repo_path][Coherence:Failed] Could not resolve local_path from DB: {e}\")\n legacy_id_path = os.path.join(self.legacy_base_path, str(dashboard_id))\n if os.path.exists(legacy_id_path) and not os.path.exists(target_path):\n return self._migrate_repo_directory(dashboard_id, legacy_id_path, target_path)\n if os.path.exists(target_path):\n self._update_repo_local_path(dashboard_id, target_path)\n return target_path\n # endregion _get_repo_path\n\n # region init_repo [C:4] [TYPE Function] [SEMANTICS git,clone,init,lock]\n # @PURPOSE: Initialize or clone a repository for a dashboard with concurrent access protection.\n # @PARAM: dashboard_id (int), remote_url (str), pat (str), repo_key (Optional[str]).\n # @PRE: dashboard_id is int, remote_url is valid Git URL, pat is provided.\n # @POST: Repository is cloned or opened at the local path. Origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository; modifies local filesystem; protected by per-dashboard lock.\n # @RETURN: Repo\n def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None, default_branch: str | None = None) -> Repo:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.init_repo\"):\n self._ensure_base_path_exists()\n repo_path = self._get_repo_path(dashboard_id, repo_key=repo_key or str(dashboard_id))\n Path(repo_path).parent.mkdir(parents=True, exist_ok=True)\n if os.path.exists(repo_path):\n logger.reason(f\"Opening existing repo at {repo_path}\", extra={\"src\": \"init_repo\"})\n try:\n repo = Repo(repo_path)\n except (InvalidGitRepositoryError, NoSuchPathError):\n logger.reason(f\"Existing path is not a Git repository, recreating: {repo_path}\", extra={\"src\": \"init_repo\"})\n stale_path = Path(repo_path)\n if stale_path.exists():\n shutil.rmtree(stale_path, ignore_errors=True)\n if stale_path.exists():\n try:\n stale_path.unlink()\n except Exception:\n pass\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n logger.reason(f\"Cloning {remote_url} to {repo_path}\", extra={\"src\": \"init_repo\"})\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n # endregion init_repo\n\n # region delete_repo [C:4] [TYPE Function] [SEMANTICS git,delete,lock]\n # @PURPOSE: Remove local repository and DB binding for a dashboard with concurrent access protection.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Local path is deleted when present and GitRepository row is removed.\n # @SIDE_EFFECT Deletes filesystem directory and DB record; protected by per-dashboard lock.\n def delete_repo(self, dashboard_id: int) -> None:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.delete_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n removed_files = False\n if os.path.exists(repo_path):\n if os.path.isdir(repo_path):\n shutil.rmtree(repo_path)\n else:\n os.remove(repo_path)\n removed_files = True\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n session.delete(db_repo)\n session.commit()\n return\n if removed_files:\n return\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_id} not found\",\n )\n except HTTPException:\n session.rollback()\n raise\n except Exception as e:\n session.rollback()\n logger.error(f\"[delete_repo][Coherence:Failed] Failed to delete repository for dashboard {dashboard_id}: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to delete repository: {e!s}\")\n finally:\n session.close()\n # endregion delete_repo\n\n # region get_repo [C:4] [TYPE Function] [SEMANTICS git,open,lock]\n # @PURPOSE: Get Repo object for a dashboard with concurrent access protection.\n # @PRE: Repository must exist on disk for the given dashboard_id.\n # @POST: Returns a GitPython Repo instance for the dashboard.\n # @RETURN: Repo\n def get_repo(self, dashboard_id: int) -> Repo:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n logger.error(f\"[get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist\")\n raise HTTPException(status_code=404, detail=f\"Repository for dashboard {dashboard_id} not found\")\n try:\n return Repo(repo_path)\n except Exception as e:\n logger.error(f\"[get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}\")\n raise HTTPException(status_code=500, detail=\"Failed to open local Git repository\")\n # endregion get_repo\n\n # region configure_identity [C:4] [TYPE Function] [SEMANTICS git,identity,lock]\n # @PURPOSE: Configure repository-local Git committer identity with concurrent access protection.\n # @PRE: dashboard_id repository exists; git_username/git_email may be empty.\n # @POST: Repository config has user.name and user.email when both identity values are provided.\n # @SIDE_EFFECT Writes to repository git config; protected by per-dashboard lock.\n def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.configure_identity\"):\n normalized_username = str(git_username or \"\").strip()\n normalized_email = str(git_email or \"\").strip()\n if not normalized_username or not normalized_email:\n return\n repo = self.get_repo(dashboard_id)\n try:\n with repo.config_writer(config_level=\"repository\") as config_writer:\n config_writer.set_value(\"user\", \"name\", normalized_username)\n config_writer.set_value(\"user\", \"email\", normalized_email)\n logger.reason(f\"Applied repository-local git identity for dashboard {dashboard_id}\", extra={\"src\": \"configure_identity\"})\n except Exception as e:\n logger.error(f\"[configure_identity][Coherence:Failed] Failed to configure git identity: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to configure git identity: {e!s}\")\n # endregion configure_identity\n\n # region close [C:2] [TYPE Function] [SEMANTICS http,cleanup,shutdown]\n # @BRIEF Gracefully close the shared HTTP client (connection pool).\n # @PRE: _http_client is initialized.\n # @POST: HTTP connections closed.\n async def close(self):\n with self._close_lock:\n if self._closed:\n return\n self._closed = True\n await self._http_client.aclose()\n # endregion close\n# #endregion GitServiceBase\n# #endregion GitServiceBase\n" }, { "contract_id": "GitServiceBranchMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_branch.py", "start_line": 1, - "end_line": 193, + "end_line": 208, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes." + "PURPOSE": "Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes (all concurrent-safe via per-dashboard locks)." }, "relations": [ { @@ -58447,6 +62724,33 @@ "value": "Infra" } }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -58468,20 +62772,20 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceBranchMixin [C:3] [TYPE Module] [SEMANTICS git, branch, checkout, list, namespace]\n# @LAYER: Infra\n# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes.\n# @RELATION USED_BY -> [GitService]\n\nimport os\nfrom datetime import datetime\n\nfrom fastapi import HTTPException\nfrom git import Repo\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceBranchMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing branch and commit operations for GitService.\nclass GitServiceBranchMixin:\n # region _ensure_gitflow_branches [TYPE Function]\n # @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin.\n # @PRE: repo is a valid GitPython Repo instance.\n # @POST: main, dev, preprod are available in local repository and pushed to origin when available.\n def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None:\n with belief_scope(\"GitService._ensure_gitflow_branches\"):\n required_branches = [\"main\", \"dev\", \"preprod\"]\n local_heads = {head.name: head for head in getattr(repo, \"heads\", [])}\n base_commit = None\n try:\n base_commit = repo.head.commit\n except Exception:\n base_commit = None\n if \"main\" in local_heads:\n base_commit = local_heads[\"main\"].commit\n if base_commit is None:\n logger.reason(\n f\"Skipping branch bootstrap for dashboard {dashboard_id}: repository has no commits\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n if \"main\" not in local_heads:\n local_heads[\"main\"] = repo.create_head(\"main\", base_commit)\n logger.reason(f\"Created local branch main for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in (\"dev\", \"preprod\"):\n if branch_name in local_heads:\n continue\n local_heads[branch_name] = repo.create_head(branch_name, local_heads[\"main\"].commit)\n logger.reason(\n f\"Created local branch {branch_name} for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n logger.reason(\n f\"Remote origin is not configured for dashboard {dashboard_id}; skipping remote branch creation\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n remote_branch_names = set()\n try:\n origin.fetch()\n for ref in origin.refs:\n remote_head = getattr(ref, \"remote_head\", None)\n if remote_head:\n remote_branch_names.add(str(remote_head))\n except Exception as e:\n logger.reason(f\"Failed to fetch origin refs: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in required_branches:\n if branch_name in remote_branch_names:\n continue\n try:\n origin.push(refspec=f\"{branch_name}:{branch_name}\")\n logger.reason(\n f\"Pushed branch {branch_name} to origin for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n except Exception as e:\n logger.error(f\"[_ensure_gitflow_branches][Coherence:Failed] Failed to push branch {branch_name} to origin: {e}\")\n raise HTTPException(\n status_code=500,\n detail=f\"Failed to create default branch '{branch_name}' on remote: {e!s}\",\n )\n try:\n repo.git.checkout(\"dev\")\n logger.reason(f\"Checked out default branch dev for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n except Exception as e:\n logger.reason(f\"Could not checkout dev branch for dashboard {dashboard_id}: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n # endregion _ensure_gitflow_branches\n\n # region list_branches [TYPE Function]\n # @PURPOSE: List all branches for a dashboard's repository.\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns a list of branch metadata dictionaries.\n # @RETURN: List[dict]\n def list_branches(self, dashboard_id: int) -> list[dict]:\n with belief_scope(\"GitService.list_branches\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Listing branches for {dashboard_id}. Refs: {repo.refs}\", extra={\"src\": \"list_branches\"})\n branches = []\n for ref in repo.refs:\n try:\n name = ref.name.replace('refs/heads/', '').replace('refs/remotes/origin/', '')\n if any(b['name'] == name for b in branches):\n continue\n branches.append({\n \"name\": name,\n \"commit_hash\": ref.commit.hexsha if hasattr(ref, 'commit') else \"0000000\",\n \"is_remote\": ref.is_remote() if hasattr(ref, 'is_remote') else False,\n \"last_updated\": datetime.fromtimestamp(ref.commit.committed_date) if hasattr(ref, 'commit') else datetime.utcnow()\n })\n except Exception as e:\n logger.reason(f\"Skipping ref {ref}: {e}\", extra={\"src\": \"list_branches\"})\n try:\n active_name = repo.active_branch.name\n if not any(b['name'] == active_name for b in branches):\n branches.append({\n \"name\": active_name, \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.utcnow()\n })\n except Exception as e:\n logger.reason(f\"Could not determine active branch: {e}\", extra={\"src\": \"list_branches\"})\n if not branches:\n branches.append({\n \"name\": \"dev\", \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.utcnow()\n })\n return branches\n # endregion list_branches\n\n # region create_branch [TYPE Function]\n # @PURPOSE: Create a new branch from an existing one.\n # @PARAM: name (str) - New branch name.\n # @PARAM: from_branch (str) - Source branch.\n # @PRE: Repository exists; name is valid; from_branch exists or repo is empty.\n # @POST: A new branch is created in the repository.\n def create_branch(self, dashboard_id: int, name: str, from_branch: str = \"main\"):\n with belief_scope(\"GitService.create_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Creating branch {name} from {from_branch}\", extra={\"src\": \"create_branch\"})\n if not repo.heads and not repo.remotes:\n logger.reason(\"Repository is empty. Creating initial commit to enable branching.\", extra={\"src\": \"create_branch\"})\n readme_path = os.path.join(repo.working_dir, \"README.md\")\n if not os.path.exists(readme_path):\n with open(readme_path, \"w\") as f:\n f.write(f\"# Dashboard {dashboard_id}\\nGit repository for Superset dashboard integration.\")\n repo.index.add([\"README.md\"])\n repo.index.commit(\"Initial commit\")\n try:\n repo.commit(from_branch)\n except Exception:\n logger.reason(f\"Source branch {from_branch} not found, using HEAD\", extra={\"src\": \"create_branch\"})\n from_branch = repo.head\n try:\n new_branch = repo.create_head(name, from_branch)\n return new_branch\n except Exception as e:\n logger.error(f\"[create_branch][Coherence:Failed] {e}\")\n raise\n # endregion create_branch\n\n # region checkout_branch [TYPE Function]\n # @PURPOSE: Switch to a specific branch.\n # @PRE: Repository exists and the specified branch name exists.\n # @POST: The repository working directory is updated to the specified branch.\n def checkout_branch(self, dashboard_id: int, name: str):\n with belief_scope(\"GitService.checkout_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Checking out branch {name}\", extra={\"src\": \"checkout_branch\"})\n repo.git.checkout(name)\n # endregion checkout_branch\n\n # region commit_changes [TYPE Function]\n # @PURPOSE: Stage and commit changes.\n # @PARAM: message (str) - Commit message.\n # @PARAM: files (List[str]) - Optional list of specific files to stage.\n # @PRE: Repository exists and has changes (dirty) or files are specified.\n # @POST: Changes are staged and a new commit is created.\n def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):\n with belief_scope(\"GitService.commit_changes\"):\n repo = self.get_repo(dashboard_id)\n if not repo.is_dirty(untracked_files=True) and not files:\n logger.reason(f\"No changes to commit for dashboard {dashboard_id}\", extra={\"src\": \"commit_changes\"})\n return\n if files:\n logger.reason(f\"Staging files: {files}\", extra={\"src\": \"commit_changes\"})\n repo.index.add(files)\n else:\n logger.reason(\"Staging all changes\", extra={\"src\": \"commit_changes\"})\n repo.git.add(A=True)\n repo.index.commit(message)\n logger.info(f\"[commit_changes][Coherence:OK] Committed changes with message: {message}\")\n # endregion commit_changes\n# #endregion GitServiceBranchMixin\n# #endregion GitServiceBranchMixin\n" + "body": "# #region GitServiceBranchMixin [C:4] [TYPE Module] [SEMANTICS git, branch, checkout, list, namespace, lock]\n# @LAYER: Infra\n# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes (all concurrent-safe via per-dashboard locks).\n# @RELATION USED_BY -> [GitService]\n\nimport os\nfrom datetime import datetime\n\nfrom fastapi import HTTPException\nfrom git import Repo\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceBranchMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing branch and commit operations for GitService.\nclass GitServiceBranchMixin:\n # region _ensure_gitflow_branches [C:4] [TYPE Function] [SEMANTICS git,gitflow,branch,lock]\n # @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin.\n # @PRE: repo is a valid GitPython Repo instance.\n # @POST: main, dev, preprod are available in local repository and pushed to origin when available.\n # Active branch unchanged (no spurious checkout).\n def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None:\n with belief_scope(\"GitService._ensure_gitflow_branches\"):\n required_branches = [\"main\", \"dev\", \"preprod\"]\n local_heads = {head.name: head for head in getattr(repo, \"heads\", [])}\n base_commit = None\n try:\n base_commit = repo.head.commit\n except Exception:\n base_commit = None\n if \"main\" in local_heads:\n base_commit = local_heads[\"main\"].commit\n if base_commit is None:\n logger.reason(\n f\"Skipping branch bootstrap for dashboard {dashboard_id}: repository has no commits\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n if \"main\" not in local_heads:\n local_heads[\"main\"] = repo.create_head(\"main\", base_commit)\n logger.reason(f\"Created local branch main for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in (\"dev\", \"preprod\"):\n if branch_name in local_heads:\n continue\n local_heads[branch_name] = repo.create_head(branch_name, local_heads[\"main\"].commit)\n logger.reason(\n f\"Created local branch {branch_name} for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n logger.reason(\n f\"Remote origin is not configured for dashboard {dashboard_id}; skipping remote branch creation\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n remote_branch_names = set()\n try:\n origin.fetch()\n for ref in origin.refs:\n remote_head = getattr(ref, \"remote_head\", None)\n if remote_head:\n remote_branch_names.add(str(remote_head))\n except Exception as e:\n logger.reason(f\"Failed to fetch origin refs: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in required_branches:\n if branch_name in remote_branch_names:\n continue\n try:\n origin.push(refspec=f\"{branch_name}:{branch_name}\")\n logger.reason(\n f\"Pushed branch {branch_name} to origin for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n except Exception as e:\n logger.error(f\"[_ensure_gitflow_branches][Coherence:Failed] Failed to push branch {branch_name} to origin: {e}\")\n raise HTTPException(\n status_code=500,\n detail=f\"Failed to create default branch '{branch_name}' on remote: {e!s}\",\n )\n # Fix 6: Only checkout if not already on dev\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = None\n if current_branch != \"dev\":\n try:\n repo.git.checkout(\"dev\")\n logger.reason(f\"Checked out default branch dev for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n except Exception as e:\n logger.reason(f\"Could not checkout dev branch for dashboard {dashboard_id}: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n # endregion _ensure_gitflow_branches\n\n # region list_branches [C:4] [TYPE Function] [SEMANTICS git,branch,list,lock]\n # @PURPOSE: List all branches (excluding tags) for a dashboard's repository, concurrent-safe.\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns a list of branch metadata dictionaries (no tag refs).\n # @RETURN: List[dict]\n def list_branches(self, dashboard_id: int) -> list[dict]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.list_branches\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Listing branches for {dashboard_id}. Refs: {repo.refs}\", extra={\"src\": \"list_branches\"})\n branches = []\n for ref in repo.refs:\n try:\n # Fix 8: Skip tag refs\n ref_name = str(ref.name)\n if ref_name.startswith('refs/tags/'):\n continue\n name = ref_name.replace('refs/heads/', '').replace('refs/remotes/origin/', '')\n if any(b['name'] == name for b in branches):\n continue\n branches.append({\n \"name\": name,\n \"commit_hash\": ref.commit.hexsha if hasattr(ref, 'commit') else \"0000000\",\n \"is_remote\": ref.is_remote() if hasattr(ref, 'is_remote') else False,\n \"last_updated\": datetime.fromtimestamp(ref.commit.committed_date) if hasattr(ref, 'commit') else datetime.utcnow()\n })\n except Exception as e:\n logger.reason(f\"Skipping ref {ref}: {e}\", extra={\"src\": \"list_branches\"})\n try:\n active_name = repo.active_branch.name\n if not any(b['name'] == active_name for b in branches):\n branches.append({\n \"name\": active_name, \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.utcnow()\n })\n except Exception as e:\n logger.reason(f\"Could not determine active branch: {e}\", extra={\"src\": \"list_branches\"})\n if not branches:\n branches.append({\n \"name\": \"dev\", \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.utcnow()\n })\n return branches\n # endregion list_branches\n\n # region create_branch [C:4] [TYPE Function] [SEMANTICS git,branch,create,lock]\n # @PURPOSE: Create a new branch from an existing one (concurrent-safe).\n # @PARAM: name (str) - New branch name.\n # @PARAM: from_branch (str) - Source branch.\n # @PRE: Repository exists; name is valid; from_branch exists or repo is empty.\n # @POST: A new branch is created in the repository.\n def create_branch(self, dashboard_id: int, name: str, from_branch: str = \"main\"):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.create_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Creating branch {name} from {from_branch}\", extra={\"src\": \"create_branch\"})\n if not repo.heads and not repo.remotes:\n logger.reason(\"Repository is empty. Creating initial commit to enable branching.\", extra={\"src\": \"create_branch\"})\n readme_path = os.path.join(repo.working_dir, \"README.md\")\n if not os.path.exists(readme_path):\n with open(readme_path, \"w\") as f:\n f.write(f\"# Dashboard {dashboard_id}\\nGit repository for Superset dashboard integration.\")\n repo.index.add([\"README.md\"])\n repo.index.commit(\"Initial commit\")\n try:\n repo.commit(from_branch)\n except Exception:\n logger.reason(f\"Source branch {from_branch} not found, using HEAD\", extra={\"src\": \"create_branch\"})\n from_branch = repo.head\n try:\n new_branch = repo.create_head(name, from_branch)\n return new_branch\n except Exception as e:\n logger.error(f\"[create_branch][Coherence:Failed] {e}\")\n raise\n # endregion create_branch\n\n # region checkout_branch [C:4] [TYPE Function] [SEMANTICS git,branch,checkout,lock]\n # @PURPOSE: Switch to a specific branch (concurrent-safe).\n # @PRE: Repository exists and the specified branch name exists.\n # @POST: The repository working directory is updated to the specified branch.\n def checkout_branch(self, dashboard_id: int, name: str):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.checkout_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Checking out branch {name}\", extra={\"src\": \"checkout_branch\"})\n repo.git.checkout(name)\n # endregion checkout_branch\n\n # region commit_changes [C:4] [TYPE Function] [SEMANTICS git,commit,stage,lock]\n # @PURPOSE: Stage and commit changes (concurrent-safe).\n # @PARAM: message (str) - Commit message.\n # @PARAM: files (List[str]) - Optional list of specific files to stage.\n # @PRE: Repository exists and has changes (dirty) or files are specified.\n # @POST: Changes are staged and a new commit is created.\n def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.commit_changes\"):\n repo = self.get_repo(dashboard_id)\n if not repo.is_dirty(untracked_files=True) and not files:\n logger.reason(f\"No changes to commit for dashboard {dashboard_id}\", extra={\"src\": \"commit_changes\"})\n return\n if files:\n logger.reason(f\"Staging files: {files}\", extra={\"src\": \"commit_changes\"})\n repo.index.add(files)\n else:\n logger.reason(\"Staging all changes\", extra={\"src\": \"commit_changes\"})\n repo.git.add(A=True)\n repo.index.commit(message)\n logger.info(f\"[commit_changes][Coherence:OK] Committed changes with message: {message}\")\n # endregion commit_changes\n# #endregion GitServiceBranchMixin\n# #endregion GitServiceBranchMixin\n" }, { "contract_id": "GitServiceGiteaMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_gitea.py", "start_line": 1, - "end_line": 261, + "end_line": 259, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation." + "PURPOSE": "Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. Uses shared self._http_client for connection pooling." }, "relations": [ { @@ -58509,6 +62813,33 @@ "value": "Infra" } }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -58530,7 +62861,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceGiteaMixin [C:3] [TYPE Module] [SEMANTICS git, gitea, api, remote, connection]\n# @LAYER: Infra\n# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation.\n# @RELATION USED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nimport httpx\nfrom fastapi import HTTPException\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.git import GitProvider\n\n\n# #region GitServiceGiteaMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing Gitea API operations for GitService.\nclass GitServiceGiteaMixin:\n # region test_connection [TYPE Function]\n # @PURPOSE: Test connection to Git provider using PAT.\n # @PARAM: provider (GitProvider), url (str), pat (str)\n # @PRE: provider is valid; url is a valid HTTP(S) URL; pat is provided.\n # @POST: Returns True if connection to the provider's API succeeds.\n # @RETURN: bool\n async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:\n with belief_scope(\"GitService.test_connection\"):\n if \".local\" in url or \"localhost\" in url:\n logger.reason(\"Local/Offline mode detected for URL\", extra={\"src\": \"test_connection\"})\n return True\n if not url.startswith(('http://', 'https://')):\n logger.error(f\"[test_connection][Coherence:Failed] Invalid URL protocol: {url}\")\n return False\n if not pat or not pat.strip():\n logger.error(\"[test_connection][Coherence:Failed] Git PAT is missing or empty\")\n return False\n pat = pat.strip()\n try:\n async with httpx.AsyncClient() as client:\n if provider == GitProvider.GITHUB:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = \"https://api.github.com/user\" if \"github.com\" in url else f\"{url.rstrip('/')}/api/v3/user\"\n resp = await client.get(api_url, headers=headers)\n elif provider == GitProvider.GITLAB:\n headers = {\"PRIVATE-TOKEN\": pat}\n api_url = f\"{url.rstrip('/')}/api/v4/user\"\n resp = await client.get(api_url, headers=headers)\n elif provider == GitProvider.GITEA:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = f\"{url.rstrip('/')}/api/v1/user\"\n resp = await client.get(api_url, headers=headers)\n else:\n return False\n if resp.status_code != 200:\n logger.error(f\"[test_connection][Coherence:Failed] Git connection test failed for {provider} at {api_url}. Status: {resp.status_code}\")\n return resp.status_code == 200\n except Exception as e:\n logger.error(f\"[test_connection][Coherence:Failed] Error testing git connection: {e}\")\n return False\n # endregion test_connection\n\n # region _gitea_headers [TYPE Function]\n # @PURPOSE: Build Gitea API authorization headers.\n # @PRE: pat is provided.\n # @POST: Returns headers with token auth.\n # @RETURN: Dict[str, str]\n def _gitea_headers(self, pat: str) -> dict[str, str]:\n token = (pat or \"\").strip()\n if not token:\n raise HTTPException(status_code=400, detail=\"Git PAT is required for Gitea operations\")\n return {\n \"Authorization\": f\"token {token}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n }\n # endregion _gitea_headers\n\n # region _gitea_request [TYPE Function]\n # @PURPOSE: Execute HTTP request against Gitea API with stable error mapping.\n # @PRE: method and endpoint are valid.\n # @POST: Returns decoded JSON payload.\n # @RETURN: Any\n async def _gitea_request(\n self, method: str, server_url: str, pat: str, endpoint: str,\n payload: dict[str, Any] | None = None,\n ) -> Any:\n base_url = self._normalize_git_server_url(server_url)\n url = f\"{base_url}/api/v1{endpoint}\"\n headers = self._gitea_headers(pat)\n try:\n async with httpx.AsyncClient(timeout=20.0) as client:\n response = await client.request(method=method, url=url, headers=headers, json=payload)\n except Exception as e:\n logger.error(f\"[gitea_request][Coherence:Failed] Network error: {e}\")\n raise HTTPException(status_code=503, detail=f\"Gitea API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or parsed.get(\"error\") or detail\n except Exception:\n pass\n logger.error(f\"[gitea_request][Coherence:Failed] method={method} endpoint={endpoint} status={response.status_code} detail={detail}\")\n raise HTTPException(status_code=response.status_code, detail=f\"Gitea API error: {detail}\")\n if response.status_code == 204:\n return None\n return response.json()\n # endregion _gitea_request\n\n # region get_gitea_current_user [TYPE Function]\n # @PURPOSE: Resolve current Gitea user for PAT.\n # @PRE: server_url and pat are valid.\n # @POST: Returns current username.\n # @RETURN: str\n async def get_gitea_current_user(self, server_url: str, pat: str) -> str:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user\")\n username = payload.get(\"login\") or payload.get(\"username\")\n if not username:\n raise HTTPException(status_code=500, detail=\"Failed to resolve Gitea username\")\n return str(username)\n # endregion get_gitea_current_user\n\n # region list_gitea_repositories [TYPE Function]\n # @PURPOSE: List repositories visible to authenticated Gitea user.\n # @PRE: server_url and pat are valid.\n # @POST: Returns repository list from Gitea.\n # @RETURN: List[dict]\n async def list_gitea_repositories(self, server_url: str, pat: str) -> list[dict]:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user/repos?limit=100&page=1\")\n if not isinstance(payload, list):\n return []\n return payload\n # endregion list_gitea_repositories\n\n # region create_gitea_repository [TYPE Function]\n # @PURPOSE: Create repository in Gitea for authenticated user.\n # @PRE: name is non-empty and PAT has repo creation permission.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_gitea_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n created = await self._gitea_request(\"POST\", server_url, pat, \"/user/repos\", payload=payload)\n if not isinstance(created, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating repository\")\n return created\n # endregion create_gitea_repository\n\n # region delete_gitea_repository [TYPE Function]\n # @PURPOSE: Delete repository in Gitea.\n # @PRE: owner and repo_name are non-empty.\n # @POST: Repository deleted on Gitea server.\n async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None:\n if not owner or not repo_name:\n raise HTTPException(status_code=400, detail=\"owner and repo_name are required\")\n await self._gitea_request(\"DELETE\", server_url, pat, f\"/repos/{owner}/{repo_name}\")\n # endregion delete_gitea_repository\n\n # region _gitea_branch_exists [TYPE Function]\n # @PURPOSE: Check whether a branch exists in Gitea repository.\n # @PRE: owner/repo/branch are non-empty.\n # @POST: Returns True when branch exists, False when 404.\n # @RETURN: bool\n async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool:\n if not owner or not repo or not branch:\n return False\n endpoint = f\"/repos/{owner}/{repo}/branches/{quote(branch, safe='')}\"\n try:\n await self._gitea_request(\"GET\", server_url, pat, endpoint)\n return True\n except HTTPException as exc:\n if exc.status_code == 404:\n return False\n raise\n # endregion _gitea_branch_exists\n\n # region _build_gitea_pr_404_detail [TYPE Function]\n # @PURPOSE: Build actionable error detail for Gitea PR 404 responses.\n # @PRE: owner/repo/from_branch/to_branch are provided.\n # @POST: Returns specific branch-missing message when detected.\n # @RETURN: Optional[str]\n async def _build_gitea_pr_404_detail(\n self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str,\n ) -> str | None:\n source_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=from_branch,\n )\n target_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=to_branch,\n )\n if not source_exists:\n return f\"Gitea branch not found: source branch '{from_branch}' in {owner}/{repo}\"\n if not target_exists:\n return f\"Gitea branch not found: target branch '{to_branch}' in {owner}/{repo}\"\n return None\n # endregion _build_gitea_pr_404_detail\n\n # region create_gitea_pull_request [TYPE Function]\n # @PURPOSE: Create pull request in Gitea.\n # @PRE: Config and remote URL are valid.\n # @POST: Returns normalized PR metadata.\n # @RETURN: Dict[str, Any]\n async def create_gitea_pull_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n req_payload = {\"title\": title, \"head\": from_branch, \"base\": to_branch, \"body\": description or \"\"}\n endpoint = f\"/repos/{identity['owner']}/{identity['repo']}/pulls\"\n active_server_url = server_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as exc:\n fallback_url = self._derive_server_url_from_remote(remote_url)\n normalized_primary = self._normalize_git_server_url(server_url)\n should_retry_with_fallback = (\n exc.status_code == 404 and fallback_url and fallback_url != normalized_primary\n )\n if should_retry_with_fallback:\n logger.explore(\n f\"Primary Gitea URL not found, retrying with remote host: {fallback_url}\",\n extra={\"src\": \"create_gitea_pull_request\"},\n )\n active_server_url = fallback_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as retry_exc:\n if retry_exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n else:\n if exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n if not isinstance(data, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating pull request\")\n return {\n \"id\": data.get(\"number\") or data.get(\"id\"),\n \"url\": data.get(\"html_url\") or data.get(\"url\"),\n \"status\": data.get(\"state\") or \"open\",\n }\n # endregion create_gitea_pull_request\n# #endregion GitServiceGiteaMixin\n# #endregion GitServiceGiteaMixin\n" + "body": "# #region GitServiceGiteaMixin [C:4] [TYPE Module] [SEMANTICS git, gitea, api, remote, connection, http_pool]\n# @LAYER: Infra\n# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. Uses shared self._http_client for connection pooling.\n# @RELATION USED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nimport httpx\nfrom fastapi import HTTPException\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.git import GitProvider\n\n\n# #region GitServiceGiteaMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing Gitea API operations for GitService.\nclass GitServiceGiteaMixin:\n # region test_connection [TYPE Function]\n # @PURPOSE: Test connection to Git provider using PAT.\n # @PARAM: provider (GitProvider), url (str), pat (str)\n # @PRE: provider is valid; url is a valid HTTP(S) URL; pat is provided.\n # @POST: Returns True if connection to the provider's API succeeds.\n # @RETURN: bool\n async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:\n with belief_scope(\"GitService.test_connection\"):\n if \".local\" in url or \"localhost\" in url:\n logger.reason(\"Local/Offline mode detected for URL\", extra={\"src\": \"test_connection\"})\n return True\n if not url.startswith(('http://', 'https://')):\n logger.error(f\"[test_connection][Coherence:Failed] Invalid URL protocol: {url}\")\n return False\n if not pat or not pat.strip():\n logger.error(\"[test_connection][Coherence:Failed] Git PAT is missing or empty\")\n return False\n pat = pat.strip()\n try:\n if provider == GitProvider.GITHUB:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = \"https://api.github.com/user\" if \"github.com\" in url else f\"{url.rstrip('/')}/api/v3/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n elif provider == GitProvider.GITLAB:\n headers = {\"PRIVATE-TOKEN\": pat}\n api_url = f\"{url.rstrip('/')}/api/v4/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n elif provider == GitProvider.GITEA:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = f\"{url.rstrip('/')}/api/v1/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n else:\n return False\n if resp.status_code != 200:\n logger.error(f\"[test_connection][Coherence:Failed] Git connection test failed for {provider} at {api_url}. Status: {resp.status_code}\")\n return resp.status_code == 200\n except Exception as e:\n logger.error(f\"[test_connection][Coherence:Failed] Error testing git connection: {e}\")\n return False\n # endregion test_connection\n\n # region _gitea_headers [TYPE Function]\n # @PURPOSE: Build Gitea API authorization headers.\n # @PRE: pat is provided.\n # @POST: Returns headers with token auth.\n # @RETURN: Dict[str, str]\n def _gitea_headers(self, pat: str) -> dict[str, str]:\n token = (pat or \"\").strip()\n if not token:\n raise HTTPException(status_code=400, detail=\"Git PAT is required for Gitea operations\")\n return {\n \"Authorization\": f\"token {token}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n }\n # endregion _gitea_headers\n\n # region _gitea_request [TYPE Function]\n # @PURPOSE: Execute HTTP request against Gitea API with stable error mapping.\n # @PRE: method and endpoint are valid.\n # @POST: Returns decoded JSON payload.\n # @RETURN: Any\n async def _gitea_request(\n self, method: str, server_url: str, pat: str, endpoint: str,\n payload: dict[str, Any] | None = None,\n ) -> Any:\n base_url = self._normalize_git_server_url(server_url)\n url = f\"{base_url}/api/v1{endpoint}\"\n headers = self._gitea_headers(pat)\n try:\n response = await self._http_client.request(method=method, url=url, headers=headers, json=payload)\n except Exception as e:\n logger.error(f\"[gitea_request][Coherence:Failed] Network error: {e}\")\n raise HTTPException(status_code=503, detail=f\"Gitea API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or parsed.get(\"error\") or detail\n except Exception:\n pass\n logger.error(f\"[gitea_request][Coherence:Failed] method={method} endpoint={endpoint} status={response.status_code} detail={detail}\")\n raise HTTPException(status_code=response.status_code, detail=f\"Gitea API error: {detail}\")\n if response.status_code == 204:\n return None\n return response.json()\n # endregion _gitea_request\n\n # region get_gitea_current_user [TYPE Function]\n # @PURPOSE: Resolve current Gitea user for PAT.\n # @PRE: server_url and pat are valid.\n # @POST: Returns current username.\n # @RETURN: str\n async def get_gitea_current_user(self, server_url: str, pat: str) -> str:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user\")\n username = payload.get(\"login\") or payload.get(\"username\")\n if not username:\n raise HTTPException(status_code=500, detail=\"Failed to resolve Gitea username\")\n return str(username)\n # endregion get_gitea_current_user\n\n # region list_gitea_repositories [TYPE Function]\n # @PURPOSE: List repositories visible to authenticated Gitea user.\n # @PRE: server_url and pat are valid.\n # @POST: Returns repository list from Gitea.\n # @RETURN: List[dict]\n async def list_gitea_repositories(self, server_url: str, pat: str) -> list[dict]:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user/repos?limit=100&page=1\")\n if not isinstance(payload, list):\n return []\n return payload\n # endregion list_gitea_repositories\n\n # region create_gitea_repository [TYPE Function]\n # @PURPOSE: Create repository in Gitea for authenticated user.\n # @PRE: name is non-empty and PAT has repo creation permission.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_gitea_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n created = await self._gitea_request(\"POST\", server_url, pat, \"/user/repos\", payload=payload)\n if not isinstance(created, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating repository\")\n return created\n # endregion create_gitea_repository\n\n # region delete_gitea_repository [TYPE Function]\n # @PURPOSE: Delete repository in Gitea.\n # @PRE: owner and repo_name are non-empty.\n # @POST: Repository deleted on Gitea server.\n async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None:\n if not owner or not repo_name:\n raise HTTPException(status_code=400, detail=\"owner and repo_name are required\")\n await self._gitea_request(\"DELETE\", server_url, pat, f\"/repos/{owner}/{repo_name}\")\n # endregion delete_gitea_repository\n\n # region _gitea_branch_exists [TYPE Function]\n # @PURPOSE: Check whether a branch exists in Gitea repository.\n # @PRE: owner/repo/branch are non-empty.\n # @POST: Returns True when branch exists, False when 404.\n # @RETURN: bool\n async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool:\n if not owner or not repo or not branch:\n return False\n endpoint = f\"/repos/{owner}/{repo}/branches/{quote(branch, safe='')}\"\n try:\n await self._gitea_request(\"GET\", server_url, pat, endpoint)\n return True\n except HTTPException as exc:\n if exc.status_code == 404:\n return False\n raise\n # endregion _gitea_branch_exists\n\n # region _build_gitea_pr_404_detail [TYPE Function]\n # @PURPOSE: Build actionable error detail for Gitea PR 404 responses.\n # @PRE: owner/repo/from_branch/to_branch are provided.\n # @POST: Returns specific branch-missing message when detected.\n # @RETURN: Optional[str]\n async def _build_gitea_pr_404_detail(\n self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str,\n ) -> str | None:\n source_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=from_branch,\n )\n target_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=to_branch,\n )\n if not source_exists:\n return f\"Gitea branch not found: source branch '{from_branch}' in {owner}/{repo}\"\n if not target_exists:\n return f\"Gitea branch not found: target branch '{to_branch}' in {owner}/{repo}\"\n return None\n # endregion _build_gitea_pr_404_detail\n\n # region create_gitea_pull_request [TYPE Function]\n # @PURPOSE: Create pull request in Gitea.\n # @PRE: Config and remote URL are valid.\n # @POST: Returns normalized PR metadata.\n # @RETURN: Dict[str, Any]\n async def create_gitea_pull_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n req_payload = {\"title\": title, \"head\": from_branch, \"base\": to_branch, \"body\": description or \"\"}\n endpoint = f\"/repos/{identity['owner']}/{identity['repo']}/pulls\"\n active_server_url = server_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as exc:\n fallback_url = self._derive_server_url_from_remote(remote_url)\n normalized_primary = self._normalize_git_server_url(server_url)\n should_retry_with_fallback = (\n exc.status_code == 404 and fallback_url and fallback_url != normalized_primary\n )\n if should_retry_with_fallback:\n logger.explore(\n f\"Primary Gitea URL not found, retrying with remote host: {fallback_url}\",\n extra={\"src\": \"create_gitea_pull_request\"},\n )\n active_server_url = fallback_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as retry_exc:\n if retry_exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n else:\n if exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n if not isinstance(data, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating pull request\")\n return {\n \"id\": data.get(\"number\") or data.get(\"id\"),\n \"url\": data.get(\"html_url\") or data.get(\"url\"),\n \"status\": data.get(\"state\") or \"open\",\n }\n # endregion create_gitea_pull_request\n# #endregion GitServiceGiteaMixin\n# #endregion GitServiceGiteaMixin\n" }, { "contract_id": "GitServiceMergeMixin", @@ -58621,12 +62952,129 @@ "tier_source": "AutoCalculated", "body": "# #region GitServiceMergeMixin [C:4] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution, lock]\n# @LAYER: Infra\n# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote (all concurrent-safe via per-dashboard locks).\n# @RELATION USED_BY -> [GitService]\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import GitCommandError\nfrom git.objects.blob import Blob\n\nfrom src.core.logger import belief_scope\n\n\n# #region GitServiceMergeMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing merge operations for GitService.\nclass GitServiceMergeMixin:\n # region _read_blob_text [TYPE Function]\n # @PURPOSE: Read text from a Git blob.\n def _read_blob_text(self, blob: Blob) -> str:\n with belief_scope(\"GitService._read_blob_text\"):\n if blob is None:\n return \"\"\n try:\n return blob.data_stream.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n return \"\"\n # endregion _read_blob_text\n\n # region _get_unmerged_file_paths [TYPE Function]\n # @PURPOSE: List files with merge conflicts.\n def _get_unmerged_file_paths(self, repo: Repo) -> list[str]:\n with belief_scope(\"GitService._get_unmerged_file_paths\"):\n try:\n return sorted(list(repo.index.unmerged_blobs().keys()))\n except Exception:\n return []\n # endregion _get_unmerged_file_paths\n\n # region _build_unfinished_merge_payload [TYPE Function]\n # @PURPOSE: Build payload for unfinished merge state.\n def _build_unfinished_merge_payload(self, repo: Repo) -> dict[str, Any]:\n with belief_scope(\"GitService._build_unfinished_merge_payload\"):\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n merge_head_value = \"\"\n merge_msg_preview = \"\"\n current_branch = \"unknown\"\n try:\n merge_head_value = Path(merge_head_path).read_text(encoding=\"utf-8\").strip()\n except Exception:\n merge_head_value = \"\"\n try:\n merge_msg_path = os.path.join(repo.git_dir, \"MERGE_MSG\")\n if os.path.exists(merge_msg_path):\n merge_msg_preview = (\n Path(merge_msg_path).read_text(encoding=\"utf-8\").strip().splitlines()[:1] or [\"\"]\n )[0]\n except Exception:\n merge_msg_preview = \"\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n conflicts_count = len(self._get_unmerged_file_paths(repo))\n return {\n \"error_code\": \"GIT_UNFINISHED_MERGE\",\n \"message\": (\n \"\\u0412 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0438 \\u0435\\u0441\\u0442\\u044c \\u043d\\u0435\\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0451\\u043d\\u043d\\u043e\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435. \"\n \"\\u0417\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u0435 \\u0438\\u043b\\u0438 \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435 \\u0432\\u0440\\u0443\\u0447\\u043d\\u0443\\u044e.\"\n ),\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": merge_head_value,\n \"merge_message_preview\": merge_msg_preview,\n \"conflicts_count\": conflicts_count,\n \"next_steps\": [\n \"\\u041e\\u0442\\u043a\\u0440\\u043e\\u0439\\u0442\\u0435 \\u043b\\u043e\\u043a\\u0430\\u043b\\u044c\\u043d\\u044b\\u0439 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0439 \\u043f\\u043e \\u043f\\u0443\\u0442\\u0438 repository_path\",\n \"\\u041f\\u0440\\u043e\\u0432\\u0435\\u0440\\u044c\\u0442\\u0435 \\u0441\\u043e\\u0441\\u0442\\u043e\\u044f\\u043d\\u0438\\u0435: git status\",\n \"\\u0420\\u0430\\u0437\\u0440\\u0435\\u0448\\u0438\\u0442\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b \\u0438 \\u0432\\u044b\\u043f\\u043e\\u043b\\u043d\\u0438\\u0442\\u0435 commit, \\u043b\\u0438\\u0431\\u043e \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435: git merge --abort\",\n \"\\u041f\\u043e\\u0441\\u043b\\u0435 \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0435\\u043d\\u0438\\u044f/\\u043e\\u0442\\u043c\\u0435\\u043d\\u044b \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u044f \\u043f\\u043e\\u0432\\u0442\\u043e\\u0440\\u0438\\u0442\\u0435 Pull \\u0438\\u0437 \\u0438\\u043d\\u0442\\u0435\\u0440\\u0444\\u0435\\u0439\\u0441\\u0430\",\n ],\n \"manual_commands\": [\"git status\", \"git add \", 'git commit -m \"resolve merge conflicts\"', \"git merge --abort\"],\n }\n # endregion _build_unfinished_merge_payload\n\n # region get_merge_status [C:4] [TYPE Function] [SEMANTICS git,merge,status,lock]\n # @PURPOSE: Get current merge status for a dashboard repository (concurrent-safe).\n def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_merge_status\"):\n repo = self.get_repo(dashboard_id)\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n if not os.path.exists(merge_head_path):\n current_branch = \"unknown\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n return {\n \"has_unfinished_merge\": False,\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": None,\n \"merge_message_preview\": None,\n \"conflicts_count\": 0,\n }\n payload = self._build_unfinished_merge_payload(repo)\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": payload[\"repository_path\"],\n \"git_dir\": payload[\"git_dir\"],\n \"current_branch\": payload[\"current_branch\"],\n \"merge_head\": payload[\"merge_head\"],\n \"merge_message_preview\": payload[\"merge_message_preview\"],\n \"conflicts_count\": int(payload.get(\"conflicts_count\") or 0),\n }\n # endregion get_merge_status\n\n # region get_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,list,lock]\n # @PURPOSE: List all files with conflicts and their contents (concurrent-safe).\n def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_merge_conflicts\"):\n repo = self.get_repo(dashboard_id)\n conflicts = []\n unmerged = repo.index.unmerged_blobs()\n for file_path, stages in unmerged.items():\n mine_blob = None\n theirs_blob = None\n for stage, blob in stages:\n if stage == 2:\n mine_blob = blob\n elif stage == 3:\n theirs_blob = blob\n conflicts.append({\n \"file_path\": file_path,\n \"mine\": self._read_blob_text(mine_blob) if mine_blob else \"\",\n \"theirs\": self._read_blob_text(theirs_blob) if theirs_blob else \"\",\n })\n return sorted(conflicts, key=lambda item: item[\"file_path\"])\n # endregion get_merge_conflicts\n\n # region resolve_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,resolve,lock]\n # @PURPOSE: Resolve conflicts using specified strategy (concurrent-safe).\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions: list[dict[str, Any]]) -> list[str]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.resolve_merge_conflicts\"):\n repo = self.get_repo(dashboard_id)\n resolved_files: list[str] = []\n repo_root = os.path.abspath(str(repo.working_tree_dir or \"\"))\n if not repo_root:\n raise HTTPException(status_code=500, detail=\"Repository working tree directory is unavailable\")\n for item in resolutions or []:\n file_path = str(item.get(\"file_path\") or \"\").strip()\n strategy = str(item.get(\"resolution\") or \"\").strip().lower()\n content = item.get(\"content\")\n if not file_path:\n raise HTTPException(status_code=400, detail=\"resolution.file_path is required\")\n if strategy not in {\"mine\", \"theirs\", \"manual\"}:\n raise HTTPException(status_code=400, detail=f\"Unsupported resolution strategy: {strategy}\")\n if strategy == \"mine\":\n repo.git.checkout(\"--ours\", \"--\", file_path)\n elif strategy == \"theirs\":\n repo.git.checkout(\"--theirs\", \"--\", file_path)\n else:\n abs_target = os.path.abspath(os.path.join(repo_root, file_path))\n if abs_target != repo_root and not abs_target.startswith(repo_root + os.sep):\n raise HTTPException(status_code=400, detail=f\"Invalid conflict file path: {file_path}\")\n os.makedirs(os.path.dirname(abs_target), exist_ok=True)\n with open(abs_target, \"w\", encoding=\"utf-8\") as file_obj:\n file_obj.write(str(content or \"\"))\n repo.git.add(file_path)\n resolved_files.append(file_path)\n return resolved_files\n # endregion resolve_merge_conflicts\n\n # region abort_merge [C:4] [TYPE Function] [SEMANTICS git,merge,abort,lock]\n # @PURPOSE: Abort ongoing merge (concurrent-safe).\n def abort_merge(self, dashboard_id: int) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.abort_merge\"):\n repo = self.get_repo(dashboard_id)\n try:\n repo.git.merge(\"--abort\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"there is no merge to abort\" in lowered or \"no merge to abort\" in lowered:\n return {\"status\": \"no_merge_in_progress\"}\n raise HTTPException(status_code=409, detail=f\"Cannot abort merge: {details}\")\n return {\"status\": \"aborted\"}\n # endregion abort_merge\n\n # region continue_merge [C:4] [TYPE Function] [SEMANTICS git,merge,continue,lock]\n # @PURPOSE: Finalize merge after conflict resolution (concurrent-safe).\n def continue_merge(self, dashboard_id: int, message: str | None = None) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.continue_merge\"):\n repo = self.get_repo(dashboard_id)\n unmerged_files = self._get_unmerged_file_paths(repo)\n if unmerged_files:\n raise HTTPException(\n status_code=409,\n detail={\n \"error_code\": \"GIT_MERGE_CONFLICTS_REMAIN\",\n \"message\": \"\\u041d\\u0435\\u0432\\u043e\\u0437\\u043c\\u043e\\u0436\\u043d\\u043e \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u044c merge: \\u043e\\u0441\\u0442\\u0430\\u043b\\u0438\\u0441\\u044c \\u043d\\u0435\\u0440\\u0430\\u0437\\u0440\\u0435\\u0448\\u0451\\u043d\\u043d\\u044b\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b.\",\n \"unresolved_files\": unmerged_files,\n },\n )\n try:\n normalized_message = str(message or \"\").strip()\n if normalized_message:\n repo.git.commit(\"-m\", normalized_message)\n else:\n repo.git.commit(\"--no-edit\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"nothing to commit\" in lowered:\n return {\"status\": \"already_clean\"}\n raise HTTPException(status_code=409, detail=f\"Cannot continue merge: {details}\")\n commit_hash = \"\"\n try:\n commit_hash = repo.head.commit.hexsha\n except Exception:\n commit_hash = \"\"\n return {\"status\": \"committed\", \"commit_hash\": commit_hash}\n # endregion continue_merge\n\n # region promote_direct_merge [C:4] [TYPE Function] [SEMANTICS git,merge,promote,branch,isolation]\n # @PURPOSE: Perform direct merge between branches with branch isolation — original branch restored on error.\n # @PRE: Repository exists and both branches are valid.\n # @POST: Target branch contains merged changes from source branch. Active branch restored to original.\n # Merge survives locally even if push fails (partial success).\n # @SIDE_EFFECT: Changes local branch state during merge; restores original branch in finally block.\n # @RETURN: Dict[str, Any]\n def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.promote_direct_merge\"):\n if not from_branch or not to_branch:\n raise HTTPException(status_code=400, detail=\"from_branch and to_branch are required\")\n repo = self.get_repo(dashboard_id)\n source = from_branch.strip()\n target = to_branch.strip()\n if source == target:\n raise HTTPException(status_code=400, detail=\"from_branch and to_branch must be different\")\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n\n # Remember original branch for restoration in finally\n original_branch = None\n try:\n original_branch = repo.active_branch.name\n except Exception:\n original_branch = None\n\n try:\n origin.fetch()\n if source not in [head.name for head in repo.heads]:\n if f\"origin/{source}\" in [ref.name for ref in repo.refs]:\n repo.git.checkout(\"-b\", source, f\"origin/{source}\")\n else:\n raise HTTPException(status_code=404, detail=f\"Source branch '{source}' not found\")\n if target in [head.name for head in repo.heads]:\n repo.git.checkout(target)\n elif f\"origin/{target}\" in [ref.name for ref in repo.refs]:\n repo.git.checkout(\"-b\", target, f\"origin/{target}\")\n else:\n raise HTTPException(status_code=404, detail=f\"Target branch '{target}' not found\")\n try:\n origin.pull(target)\n except Exception:\n pass\n repo.git.merge(source, \"--no-ff\", \"-m\", f\"chore(flow): promote {source} -> {target}\")\n origin.push(refspec=f\"{target}:{target}\")\n except HTTPException:\n raise\n except Exception as e:\n message = str(e)\n if \"CONFLICT\" in message.upper():\n raise HTTPException(status_code=409, detail=f\"Merge conflict during direct promote: {message}\")\n raise HTTPException(status_code=500, detail=f\"Direct promote failed: {message}\")\n finally:\n # Restore original branch even if merge/push partially failed\n if original_branch:\n try:\n repo.git.checkout(original_branch)\n except Exception:\n pass\n return {\"mode\": \"direct\", \"from_branch\": source, \"to_branch\": target, \"status\": \"merged\"}\n # endregion promote_direct_merge\n# #endregion GitServiceMergeMixin\n# #endregion GitServiceMergeMixin\n" }, + { + "contract_id": "GitServiceRemoteMixin", + "contract_type": "Module", + "file_path": "backend/src/services/git/_remote_providers.py", + "start_line": 1, + "end_line": 139, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "LAYER": "Infra", + "PURPOSE": "GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. Uses shared self._http_client for connection pooling." + }, + "relations": [ + { + "source_id": "GitServiceRemoteMixin", + "relation_type": "USED_BY", + "target_id": "GitService", + "target_ref": "[GitService]" + } + ], + "schema_warnings": [ + { + "code": "invalid_enum_value", + "tag": "LAYER", + "message": "@LAYER value 'Infra' is not in allowed enum", + "detail": { + "allowed": [ + "Core", + "Domain", + "API", + "UI", + "Service", + "Infrastructure", + "Plugin" + ], + "value": "Infra" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USED_BY is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USED_BY" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region GitServiceRemoteMixin [C:4] [TYPE Module] [SEMANTICS git, provider, github, remote, url, http_pool]\n# @LAYER: Infra\n# @BRIEF GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. Uses shared self._http_client for connection pooling.\n# @RELATION USED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nimport httpx\nfrom fastapi import HTTPException\n\n\n# #region GitServiceGithubMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitHub API operations for GitService.\nclass GitServiceGithubMixin:\n # region create_github_repository [TYPE Function]\n # @PURPOSE: Create repository in GitHub or GitHub Enterprise.\n # @PRE: PAT has repository create permission.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_github_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n if \"github.com\" in base_url:\n api_url = \"https://api.github.com/user/repos\"\n else:\n api_url = f\"{base_url}/api/v3/user/repos\"\n headers = {\n \"Authorization\": f\"token {pat.strip()}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/vnd.github+json\",\n }\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitHub API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitHub API error: {detail}\")\n return response.json()\n # endregion create_github_pull_request\n# #endregion GitServiceGithubMixin\n\n# #region GitServiceGitlabMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitLab API operations for GitService.\nclass GitServiceGitlabMixin:\n # region create_gitlab_repository [TYPE Function]\n # @PURPOSE: Create repository(project) in GitLab.\n # @PRE: PAT has api scope.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_gitlab_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n api_url = f\"{base_url}/api/v4/projects\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload: dict[str, Any] = {\n \"name\": name, \"visibility\": \"private\" if private else \"public\",\n \"initialize_with_readme\": bool(auto_init),\n }\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n if \"clone_url\" not in data:\n data[\"clone_url\"] = data.get(\"http_url_to_repo\")\n if \"html_url\" not in data:\n data[\"html_url\"] = data.get(\"web_url\")\n if \"ssh_url\" not in data:\n data[\"ssh_url\"] = data.get(\"ssh_url_to_repo\")\n if \"full_name\" not in data:\n data[\"full_name\"] = data.get(\"path_with_namespace\") or data.get(\"name\")\n return data\n # endregion create_gitlab_repository\n\n # region create_gitlab_merge_request [TYPE Function]\n # @PURPOSE: Create merge request in GitLab.\n # @PRE: Config and remote URL are valid.\n # @POST: Returns normalized MR metadata.\n # @RETURN: Dict[str, Any]\n async def create_gitlab_merge_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None, remove_source_branch: bool = False,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n base_url = self._normalize_git_server_url(server_url)\n project_id = quote(identity[\"full_name\"], safe=\"\")\n api_url = f\"{base_url}/api/v4/projects/{project_id}/merge_requests\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload = {\n \"source_branch\": from_branch, \"target_branch\": to_branch, \"title\": title,\n \"description\": description or \"\", \"remove_source_branch\": bool(remove_source_branch),\n }\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n return {\"id\": data.get(\"iid\") or data.get(\"id\"), \"url\": data.get(\"web_url\") or data.get(\"url\"), \"status\": data.get(\"state\") or \"opened\"}\n # endregion create_gitlab_merge_request\n# #endregion GitServiceGitlabMixin\n# #endregion GitServiceRemoteMixin\n" + }, + { + "contract_id": "GitServiceGithubMixin", + "contract_type": "Class", + "file_path": "backend/src/services/git/_remote_providers.py", + "start_line": 13, + "end_line": 54, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Mixin providing GitHub API operations for GitService." + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region GitServiceGithubMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitHub API operations for GitService.\nclass GitServiceGithubMixin:\n # region create_github_repository [TYPE Function]\n # @PURPOSE: Create repository in GitHub or GitHub Enterprise.\n # @PRE: PAT has repository create permission.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_github_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n if \"github.com\" in base_url:\n api_url = \"https://api.github.com/user/repos\"\n else:\n api_url = f\"{base_url}/api/v3/user/repos\"\n headers = {\n \"Authorization\": f\"token {pat.strip()}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/vnd.github+json\",\n }\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitHub API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitHub API error: {detail}\")\n return response.json()\n # endregion create_github_pull_request\n# #endregion GitServiceGithubMixin\n" + }, { "contract_id": "GitServiceGitlabMixin", "contract_type": "Class", "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 97, - "end_line": 181, + "start_line": 56, + "end_line": 138, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -58647,20 +63095,20 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceGitlabMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitLab API operations for GitService.\nclass GitServiceGitlabMixin:\n # region create_gitlab_repository [TYPE Function]\n # @PURPOSE: Create repository(project) in GitLab.\n # @PRE: PAT has api scope.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_gitlab_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n api_url = f\"{base_url}/api/v4/projects\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload: dict[str, Any] = {\n \"name\": name, \"visibility\": \"private\" if private else \"public\",\n \"initialize_with_readme\": bool(auto_init),\n }\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n async with httpx.AsyncClient(timeout=20.0) as client:\n response = await client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n if \"clone_url\" not in data:\n data[\"clone_url\"] = data.get(\"http_url_to_repo\")\n if \"html_url\" not in data:\n data[\"html_url\"] = data.get(\"web_url\")\n if \"ssh_url\" not in data:\n data[\"ssh_url\"] = data.get(\"ssh_url_to_repo\")\n if \"full_name\" not in data:\n data[\"full_name\"] = data.get(\"path_with_namespace\") or data.get(\"name\")\n return data\n # endregion create_gitlab_repository\n\n # region create_gitlab_merge_request [TYPE Function]\n # @PURPOSE: Create merge request in GitLab.\n # @PRE: Config and remote URL are valid.\n # @POST: Returns normalized MR metadata.\n # @RETURN: Dict[str, Any]\n async def create_gitlab_merge_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None, remove_source_branch: bool = False,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n base_url = self._normalize_git_server_url(server_url)\n project_id = quote(identity[\"full_name\"], safe=\"\")\n api_url = f\"{base_url}/api/v4/projects/{project_id}/merge_requests\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload = {\n \"source_branch\": from_branch, \"target_branch\": to_branch, \"title\": title,\n \"description\": description or \"\", \"remove_source_branch\": bool(remove_source_branch),\n }\n try:\n async with httpx.AsyncClient(timeout=20.0) as client:\n response = await client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n return {\"id\": data.get(\"iid\") or data.get(\"id\"), \"url\": data.get(\"web_url\") or data.get(\"url\"), \"status\": data.get(\"state\") or \"opened\"}\n # endregion create_gitlab_merge_request\n# #endregion GitServiceGitlabMixin\n" + "body": "# #region GitServiceGitlabMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitLab API operations for GitService.\nclass GitServiceGitlabMixin:\n # region create_gitlab_repository [TYPE Function]\n # @PURPOSE: Create repository(project) in GitLab.\n # @PRE: PAT has api scope.\n # @POST: Returns created repository payload.\n # @RETURN: dict\n async def create_gitlab_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n api_url = f\"{base_url}/api/v4/projects\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload: dict[str, Any] = {\n \"name\": name, \"visibility\": \"private\" if private else \"public\",\n \"initialize_with_readme\": bool(auto_init),\n }\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n if \"clone_url\" not in data:\n data[\"clone_url\"] = data.get(\"http_url_to_repo\")\n if \"html_url\" not in data:\n data[\"html_url\"] = data.get(\"web_url\")\n if \"ssh_url\" not in data:\n data[\"ssh_url\"] = data.get(\"ssh_url_to_repo\")\n if \"full_name\" not in data:\n data[\"full_name\"] = data.get(\"path_with_namespace\") or data.get(\"name\")\n return data\n # endregion create_gitlab_repository\n\n # region create_gitlab_merge_request [TYPE Function]\n # @PURPOSE: Create merge request in GitLab.\n # @PRE: Config and remote URL are valid.\n # @POST: Returns normalized MR metadata.\n # @RETURN: Dict[str, Any]\n async def create_gitlab_merge_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None, remove_source_branch: bool = False,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n base_url = self._normalize_git_server_url(server_url)\n project_id = quote(identity[\"full_name\"], safe=\"\")\n api_url = f\"{base_url}/api/v4/projects/{project_id}/merge_requests\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload = {\n \"source_branch\": from_branch, \"target_branch\": to_branch, \"title\": title,\n \"description\": description or \"\", \"remove_source_branch\": bool(remove_source_branch),\n }\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n return {\"id\": data.get(\"iid\") or data.get(\"id\"), \"url\": data.get(\"web_url\") or data.get(\"url\"), \"status\": data.get(\"state\") or \"opened\"}\n # endregion create_gitlab_merge_request\n# #endregion GitServiceGitlabMixin\n" }, { "contract_id": "GitServiceStatusMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_status.py", "start_line": 1, - "end_line": 159, + "end_line": 162, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues." + "PURPOSE": "Status, diff, and commit history operations for GitService (all concurrent-safe via per-dashboard locks)." }, "relations": [ { @@ -58688,6 +63136,33 @@ "value": "Infra" } }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -58709,20 +63184,20 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceStatusMixin [C:3] [TYPE Module] [SEMANTICS git, status, diff, log, history]\n# @LAYER: Infra\n# @BRIEF Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues.\n# @RELATION USED_BY -> [GitService]\n\nfrom datetime import datetime\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceStatusMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing repository status, diff, and commit history for GitService.\nclass GitServiceStatusMixin:\n # region _parse_status_porcelain [TYPE Function]\n # @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.\n # @PRE: `repo` is an open GitPython Repo instance.\n # @POST: Returns (staged, modified, untracked) tuple of file path lists.\n # @RATIONALE: Avoids repo.is_dirty() / repo.index.diff(\"HEAD\") which internally\n # call git diff --cached, a flag unsupported in some Git environments (exit 129).\n # Using git status --porcelain is self-contained and avoids the --cached flag entirely.\n def _parse_status_porcelain(self, repo) -> tuple[list[str], list[str], list[str]]:\n with belief_scope(\"GitService._parse_status_porcelain\"):\n staged: list[str] = []\n modified: list[str] = []\n untracked: list[str] = []\n try:\n output = repo.git.status(\"--porcelain\")\n except Exception:\n logger.warning(\"[_parse_status_porcelain][Coherence:Failed] git status --porcelain failed\")\n return staged, modified, untracked\n for line in output.split(\"\\n\"):\n if not line:\n continue\n if line.startswith(\"??\"):\n untracked.append(line[2:].strip())\n continue\n if line.startswith(\"!!\"):\n continue\n if len(line) < 3:\n continue\n x = line[0]\n y = line[1]\n rest = line[3:]\n path = rest.split(\" -> \")[-1].strip() if \" -> \" in rest else rest.strip()\n if x != \" \" and x != \"?\":\n staged.append(path)\n if y != \" \" and y != \"?\":\n if path not in modified:\n modified.append(path)\n return staged, modified, untracked\n # endregion _parse_status_porcelain\n\n # region get_status [TYPE Function]\n # @PURPOSE: Get current repository status (dirty files, untracked, etc.)\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns a dictionary representing the Git status.\n # @RETURN: dict\n def get_status(self, dashboard_id: int) -> dict:\n with belief_scope(\"GitService.get_status\"):\n repo = self.get_repo(dashboard_id)\n has_commits = False\n try:\n repo.head.commit\n has_commits = True\n except (ValueError, Exception):\n has_commits = False\n current_branch = repo.active_branch.name\n tracking_branch = None\n has_upstream = False\n ahead_count = 0\n behind_count = 0\n try:\n tracking_branch = repo.active_branch.tracking_branch()\n has_upstream = tracking_branch is not None\n except Exception:\n tracking_branch = None\n has_upstream = False\n if has_upstream and tracking_branch is not None:\n try:\n ahead_count = sum(1 for _ in repo.iter_commits(f\"{tracking_branch.name}..{current_branch}\"))\n behind_count = sum(1 for _ in repo.iter_commits(f\"{current_branch}..{tracking_branch.name}\"))\n except Exception:\n ahead_count = 0\n behind_count = 0\n staged_files, modified_files, untracked_files = self._parse_status_porcelain(repo)\n is_dirty = bool(staged_files or modified_files or untracked_files)\n is_diverged = ahead_count > 0 and behind_count > 0\n if is_diverged:\n sync_state = \"DIVERGED\"\n elif behind_count > 0:\n sync_state = \"BEHIND_REMOTE\"\n elif ahead_count > 0:\n sync_state = \"AHEAD_REMOTE\"\n elif is_dirty or modified_files or staged_files or untracked_files:\n sync_state = \"CHANGES\"\n else:\n sync_state = \"SYNCED\"\n return {\n \"is_dirty\": is_dirty,\n \"untracked_files\": untracked_files,\n \"modified_files\": modified_files,\n \"staged_files\": staged_files,\n \"current_branch\": current_branch,\n \"upstream_branch\": tracking_branch.name if tracking_branch is not None else None,\n \"has_upstream\": has_upstream,\n \"ahead_count\": ahead_count,\n \"behind_count\": behind_count,\n \"is_diverged\": is_diverged,\n \"sync_state\": sync_state,\n }\n # endregion get_status\n\n # region get_diff [TYPE Function]\n # @PURPOSE: Generate diff for a file or the whole repository.\n # @PARAM: file_path (str) - Optional specific file.\n # @PARAM: staged (bool) - Whether to show staged changes.\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns the diff text as a string.\n # @RETURN: str\n def get_diff(self, dashboard_id: int, file_path: str = None, staged: bool = False) -> str:\n with belief_scope(\"GitService.get_diff\"):\n repo = self.get_repo(dashboard_id)\n diff_args = []\n if staged:\n diff_args.append(\"--staged\")\n if file_path:\n return repo.git.diff(*diff_args, \"--\", file_path)\n return repo.git.diff(*diff_args)\n # endregion get_diff\n\n # region get_commit_history [TYPE Function]\n # @PURPOSE: Retrieve commit history for a repository.\n # @PARAM: limit (int) - Max number of commits to return.\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns a list of dictionaries for each commit in history.\n # @RETURN: List[dict]\n def get_commit_history(self, dashboard_id: int, limit: int = 50) -> list[dict]:\n with belief_scope(\"GitService.get_commit_history\"):\n repo = self.get_repo(dashboard_id)\n commits = []\n try:\n if not repo.heads and not repo.remotes:\n return []\n for commit in repo.iter_commits(max_count=limit):\n commits.append({\n \"hash\": commit.hexsha,\n \"author\": commit.author.name,\n \"email\": commit.author.email,\n \"timestamp\": datetime.fromtimestamp(commit.committed_date),\n \"message\": commit.message.strip(),\n \"files_changed\": list(commit.stats.files.keys())\n })\n except Exception as e:\n logger.explore(f\"Could not retrieve commit history for dashboard {dashboard_id}: {e}\", extra={\"src\": \"get_commit_history\"})\n return []\n return commits\n # endregion get_commit_history\n# #endregion GitServiceStatusMixin\n# #endregion GitServiceStatusMixin\n" + "body": "# #region GitServiceStatusMixin [C:4] [TYPE Module] [SEMANTICS git, status, diff, log, history, lock]\n# @LAYER: Infra\n# @BRIEF Status, diff, and commit history operations for GitService (all concurrent-safe via per-dashboard locks).\n# @RELATION USED_BY -> [GitService]\n\nfrom datetime import datetime\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceStatusMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing repository status, diff, and commit history for GitService.\nclass GitServiceStatusMixin:\n # region _parse_status_porcelain [TYPE Function]\n # @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.\n # @PRE: `repo` is an open GitPython Repo instance.\n # @POST: Returns (staged, modified, untracked) tuple of file path lists.\n # @RATIONALE: Avoids repo.is_dirty() / repo.index.diff(\"HEAD\") which internally\n # call git diff --cached, a flag unsupported in some Git environments (exit 129).\n # Using git status --porcelain is self-contained and avoids the --cached flag entirely.\n def _parse_status_porcelain(self, repo) -> tuple[list[str], list[str], list[str]]:\n with belief_scope(\"GitService._parse_status_porcelain\"):\n staged: list[str] = []\n modified: list[str] = []\n untracked: list[str] = []\n try:\n output = repo.git.status(\"--porcelain\")\n except Exception:\n logger.warning(\"[_parse_status_porcelain][Coherence:Failed] git status --porcelain failed\")\n return staged, modified, untracked\n for line in output.split(\"\\n\"):\n if not line:\n continue\n if line.startswith(\"??\"):\n untracked.append(line[2:].strip())\n continue\n if line.startswith(\"!!\"):\n continue\n if len(line) < 3:\n continue\n x = line[0]\n y = line[1]\n rest = line[3:]\n path = rest.split(\" -> \")[-1].strip() if \" -> \" in rest else rest.strip()\n if x != \" \" and x != \"?\":\n staged.append(path)\n if y != \" \" and y != \"?\":\n if path not in modified:\n modified.append(path)\n return staged, modified, untracked\n # endregion _parse_status_porcelain\n\n # region get_status [C:4] [TYPE Function] [SEMANTICS git,status,lock]\n # @PURPOSE: Get current repository status (concurrent-safe).\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns a dictionary representing the Git status.\n # @RETURN: dict\n def get_status(self, dashboard_id: int) -> dict:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_status\"):\n repo = self.get_repo(dashboard_id)\n has_commits = False\n try:\n repo.head.commit\n has_commits = True\n except (ValueError, Exception):\n has_commits = False\n current_branch = repo.active_branch.name\n tracking_branch = None\n has_upstream = False\n ahead_count = 0\n behind_count = 0\n try:\n tracking_branch = repo.active_branch.tracking_branch()\n has_upstream = tracking_branch is not None\n except Exception:\n tracking_branch = None\n has_upstream = False\n if has_upstream and tracking_branch is not None:\n try:\n ahead_count = sum(1 for _ in repo.iter_commits(f\"{tracking_branch.name}..{current_branch}\"))\n behind_count = sum(1 for _ in repo.iter_commits(f\"{current_branch}..{tracking_branch.name}\"))\n except Exception:\n ahead_count = 0\n behind_count = 0\n staged_files, modified_files, untracked_files = self._parse_status_porcelain(repo)\n is_dirty = bool(staged_files or modified_files or untracked_files)\n is_diverged = ahead_count > 0 and behind_count > 0\n if is_diverged:\n sync_state = \"DIVERGED\"\n elif behind_count > 0:\n sync_state = \"BEHIND_REMOTE\"\n elif ahead_count > 0:\n sync_state = \"AHEAD_REMOTE\"\n elif is_dirty or modified_files or staged_files or untracked_files:\n sync_state = \"CHANGES\"\n else:\n sync_state = \"SYNCED\"\n return {\n \"is_dirty\": is_dirty,\n \"untracked_files\": untracked_files,\n \"modified_files\": modified_files,\n \"staged_files\": staged_files,\n \"current_branch\": current_branch,\n \"upstream_branch\": tracking_branch.name if tracking_branch is not None else None,\n \"has_upstream\": has_upstream,\n \"ahead_count\": ahead_count,\n \"behind_count\": behind_count,\n \"is_diverged\": is_diverged,\n \"sync_state\": sync_state,\n }\n # endregion get_status\n\n # region get_diff [C:4] [TYPE Function] [SEMANTICS git,diff,lock]\n # @PURPOSE: Generate diff for a file or the whole repository (concurrent-safe).\n # @PARAM: file_path (str) - Optional specific file.\n # @PARAM: staged (bool) - Whether to show staged changes.\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns the diff text as a string.\n # @RETURN: str\n def get_diff(self, dashboard_id: int, file_path: str = None, staged: bool = False) -> str:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_diff\"):\n repo = self.get_repo(dashboard_id)\n diff_args = []\n if staged:\n diff_args.append(\"--staged\")\n if file_path:\n return repo.git.diff(*diff_args, \"--\", file_path)\n return repo.git.diff(*diff_args)\n # endregion get_diff\n\n # region get_commit_history [C:4] [TYPE Function] [SEMANTICS git,history,lock]\n # @PURPOSE: Retrieve commit history for a repository (concurrent-safe).\n # @PARAM: limit (int) - Max number of commits to return.\n # @PRE: Repository for dashboard_id exists.\n # @POST: Returns a list of dictionaries for each commit in history.\n # @RETURN: List[dict]\n def get_commit_history(self, dashboard_id: int, limit: int = 50) -> list[dict]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_commit_history\"):\n repo = self.get_repo(dashboard_id)\n commits = []\n try:\n if not repo.heads and not repo.remotes:\n return []\n for commit in repo.iter_commits(max_count=limit):\n commits.append({\n \"hash\": commit.hexsha,\n \"author\": commit.author.name,\n \"email\": commit.author.email,\n \"timestamp\": datetime.fromtimestamp(commit.committed_date),\n \"message\": commit.message.strip(),\n \"files_changed\": list(commit.stats.files.keys())\n })\n except Exception as e:\n logger.explore(f\"Could not retrieve commit history for dashboard {dashboard_id}: {e}\", extra={\"src\": \"get_commit_history\"})\n return []\n return commits\n # endregion get_commit_history\n# #endregion GitServiceStatusMixin\n# #endregion GitServiceStatusMixin\n" }, { "contract_id": "GitServiceSyncMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_sync.py", "start_line": 1, - "end_line": 173, + "end_line": 175, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Push and pull operations for GitService with origin host auto-alignment." + "PURPOSE": "Push and pull operations for GitService with origin host auto-alignment (concurrent-safe)." }, "relations": [ { @@ -58756,6 +63231,33 @@ "value": "Infra" } }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -58777,7 +63279,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceSyncMixin [C:3] [TYPE Module] [SEMANTICS git, sync, push, pull, remote]\n# @LAYER: Infra\n# @BRIEF Push and pull operations for GitService with origin host auto-alignment.\n# @RELATION USED_BY -> [GitService]\n# @RELATION DEPENDS_ON -> [GitServiceUrlMixin]\n\nimport os\n\nfrom fastapi import HTTPException\nfrom git.exc import GitCommandError\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.git import GitRepository, GitServerConfig\n\n\n# #region GitServiceSyncMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing push and pull operations with origin host alignment.\nclass GitServiceSyncMixin:\n # region push_changes [TYPE Function]\n # @PURPOSE: Push local commits to remote.\n # @PRE: Repository exists and has an 'origin' remote.\n # @POST: Local branch commits are pushed to origin.\n def push_changes(self, dashboard_id: int):\n with belief_scope(\"GitService.push_changes\"):\n repo = self.get_repo(dashboard_id)\n if not repo.heads:\n logger.warning(f\"[push_changes][Coherence:Failed] No local branches to push for dashboard {dashboard_id}\")\n return\n try:\n origin = repo.remote(name='origin')\n except ValueError:\n logger.error(f\"[push_changes][Coherence:Failed] Remote 'origin' not found for dashboard {dashboard_id}\")\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n try:\n origin_urls = list(origin.urls)\n except Exception:\n origin_urls = []\n binding_remote_url = None\n binding_config_id = None\n binding_config_url = None\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n binding_remote_url = db_repo.remote_url\n binding_config_id = db_repo.config_id\n db_config = (\n session.query(GitServerConfig)\n .filter(GitServerConfig.id == db_repo.config_id)\n .first()\n )\n if db_config:\n binding_config_url = db_config.url\n finally:\n session.close()\n except Exception as diag_error:\n logger.reason(\n \"Failed to load repository binding diagnostics\",\n extra={\"src\": \"push_changes\", \"dashboard_id\": dashboard_id, \"error\": str(diag_error)},\n )\n realigned_origin_url = self._align_origin_host_with_config(\n dashboard_id=dashboard_id,\n origin=origin,\n config_url=binding_config_url,\n current_origin_url=(origin_urls[0] if origin_urls else None),\n binding_remote_url=binding_remote_url,\n )\n try:\n origin_urls = list(origin.urls)\n except Exception:\n origin_urls = []\n logger.reason(\n f\"Push diagnostics dashboard={dashboard_id} config_id={binding_config_id} config_url={binding_config_url} binding_remote_url={binding_remote_url} origin_urls={origin_urls} origin_realigned={bool(realigned_origin_url)}\",\n extra={\"src\": \"push_changes\"},\n )\n try:\n current_branch = repo.active_branch\n logger.reason(f\"Pushing branch {current_branch.name} to origin\", extra={\"src\": \"push_changes\"})\n tracking_branch = None\n try:\n tracking_branch = current_branch.tracking_branch()\n except Exception:\n tracking_branch = None\n if tracking_branch is None:\n repo.git.push(\"--set-upstream\", \"origin\", f\"{current_branch.name}:{current_branch.name}\")\n else:\n push_info = origin.push(refspec=f'{current_branch.name}:{current_branch.name}')\n for info in push_info:\n if info.flags & info.ERROR:\n logger.error(f\"[push_changes][Coherence:Failed] Error pushing ref {info.remote_ref_string}: {info.summary}\")\n raise Exception(f\"Git push error for {info.remote_ref_string}: {info.summary}\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"non-fast-forward\" in lowered or \"rejected\" in lowered:\n raise HTTPException(\n status_code=409,\n detail=\"Push rejected: remote branch contains newer commits. Run Pull first, resolve conflicts if any, then push again.\",\n )\n logger.error(f\"[push_changes][Coherence:Failed] Failed to push changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git push failed: {details}\")\n except Exception as e:\n logger.error(f\"[push_changes][Coherence:Failed] Failed to push changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git push failed: {e!s}\")\n # endregion push_changes\n\n # region pull_changes [TYPE Function]\n # @PURPOSE: Pull changes from remote.\n # @PRE: Repository exists and has an 'origin' remote.\n # @POST: Changes from origin are pulled and merged into the active branch.\n def pull_changes(self, dashboard_id: int):\n with belief_scope(\"GitService.pull_changes\"):\n repo = self.get_repo(dashboard_id)\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n if os.path.exists(merge_head_path):\n payload = self._build_unfinished_merge_payload(repo)\n logger.reason(\n f\"Unfinished merge detected for dashboard {dashboard_id} (repo_path={payload['repository_path']} git_dir={payload['git_dir']} branch={payload['current_branch']} merge_head={payload['merge_head']} merge_msg={payload['merge_message_preview']})\",\n extra={\"src\": \"pull_changes\"},\n )\n raise HTTPException(status_code=409, detail=payload)\n try:\n origin = repo.remote(name='origin')\n current_branch = repo.active_branch.name\n try:\n origin_urls = list(origin.urls)\n except Exception:\n origin_urls = []\n logger.reason(\n f\"Pull diagnostics dashboard={dashboard_id} repo_path={repo.working_tree_dir} branch={current_branch} origin_urls={origin_urls}\",\n extra={\"src\": \"pull_changes\"},\n )\n origin.fetch(prune=True)\n remote_ref = f\"origin/{current_branch}\"\n has_remote_branch = any(ref.name == remote_ref for ref in repo.refs)\n logger.reason(\n f\"Pull remote branch check dashboard={dashboard_id} branch={current_branch} remote_ref={remote_ref} exists={has_remote_branch}\",\n extra={\"src\": \"pull_changes\"},\n )\n if not has_remote_branch:\n raise HTTPException(\n status_code=409,\n detail=f\"Remote branch '{current_branch}' does not exist yet. Push this branch first.\",\n )\n logger.reason(f\"Pulling changes from origin/{current_branch}\", extra={\"src\": \"pull_changes\"})\n repo.git.pull(\"--no-rebase\", \"origin\", current_branch)\n except ValueError:\n logger.error(f\"[pull_changes][Coherence:Failed] Remote 'origin' not found for dashboard {dashboard_id}\")\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"conflict\" in lowered or \"not possible to fast-forward\" in lowered:\n raise HTTPException(\n status_code=409,\n detail=\"Pull requires conflict resolution. Resolve conflicts in repository and repeat operation.\",\n )\n logger.error(f\"[pull_changes][Coherence:Failed] Failed to pull changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git pull failed: {details}\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(f\"[pull_changes][Coherence:Failed] Failed to pull changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git pull failed: {e!s}\")\n # endregion pull_changes\n# #endregion GitServiceSyncMixin\n# #endregion GitServiceSyncMixin\n" + "body": "# #region GitServiceSyncMixin [C:4] [TYPE Module] [SEMANTICS git, sync, push, pull, remote, lock]\n# @LAYER: Infra\n# @BRIEF Push and pull operations for GitService with origin host auto-alignment (concurrent-safe).\n# @RELATION USED_BY -> [GitService]\n# @RELATION DEPENDS_ON -> [GitServiceUrlMixin]\n\nimport os\n\nfrom fastapi import HTTPException\nfrom git.exc import GitCommandError\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.git import GitRepository, GitServerConfig\n\n\n# #region GitServiceSyncMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing push and pull operations with origin host alignment.\nclass GitServiceSyncMixin:\n # region push_changes [C:4] [TYPE Function] [SEMANTICS git,push,lock]\n # @PURPOSE: Push local commits to remote (concurrent-safe).\n # @PRE: Repository exists and has an 'origin' remote.\n # @POST: Local branch commits are pushed to origin.\n def push_changes(self, dashboard_id: int):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.push_changes\"):\n repo = self.get_repo(dashboard_id)\n if not repo.heads:\n logger.warning(f\"[push_changes][Coherence:Failed] No local branches to push for dashboard {dashboard_id}\")\n return\n try:\n origin = repo.remote(name='origin')\n except ValueError:\n logger.error(f\"[push_changes][Coherence:Failed] Remote 'origin' not found for dashboard {dashboard_id}\")\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n try:\n origin_urls = list(origin.urls)\n except Exception:\n origin_urls = []\n binding_remote_url = None\n binding_config_id = None\n binding_config_url = None\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n binding_remote_url = db_repo.remote_url\n binding_config_id = db_repo.config_id\n db_config = (\n session.query(GitServerConfig)\n .filter(GitServerConfig.id == db_repo.config_id)\n .first()\n )\n if db_config:\n binding_config_url = db_config.url\n finally:\n session.close()\n except Exception as diag_error:\n logger.reason(\n \"Failed to load repository binding diagnostics\",\n extra={\"src\": \"push_changes\", \"dashboard_id\": dashboard_id, \"error\": str(diag_error)},\n )\n realigned_origin_url = self._align_origin_host_with_config(\n dashboard_id=dashboard_id,\n origin=origin,\n config_url=binding_config_url,\n current_origin_url=(origin_urls[0] if origin_urls else None),\n binding_remote_url=binding_remote_url,\n )\n try:\n origin_urls = list(origin.urls)\n except Exception:\n origin_urls = []\n logger.reason(\n f\"Push diagnostics dashboard={dashboard_id} config_id={binding_config_id} config_url={binding_config_url} binding_remote_url={binding_remote_url} origin_urls={origin_urls} origin_realigned={bool(realigned_origin_url)}\",\n extra={\"src\": \"push_changes\"},\n )\n try:\n current_branch = repo.active_branch\n logger.reason(f\"Pushing branch {current_branch.name} to origin\", extra={\"src\": \"push_changes\"})\n tracking_branch = None\n try:\n tracking_branch = current_branch.tracking_branch()\n except Exception:\n tracking_branch = None\n if tracking_branch is None:\n repo.git.push(\"--set-upstream\", \"origin\", f\"{current_branch.name}:{current_branch.name}\")\n else:\n push_info = origin.push(refspec=f'{current_branch.name}:{current_branch.name}')\n for info in push_info:\n if info.flags & info.ERROR:\n logger.error(f\"[push_changes][Coherence:Failed] Error pushing ref {info.remote_ref_string}: {info.summary}\")\n raise Exception(f\"Git push error for {info.remote_ref_string}: {info.summary}\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"non-fast-forward\" in lowered or \"rejected\" in lowered:\n raise HTTPException(\n status_code=409,\n detail=\"Push rejected: remote branch contains newer commits. Run Pull first, resolve conflicts if any, then push again.\",\n )\n logger.error(f\"[push_changes][Coherence:Failed] Failed to push changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git push failed: {details}\")\n except Exception as e:\n logger.error(f\"[push_changes][Coherence:Failed] Failed to push changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git push failed: {e!s}\")\n # endregion push_changes\n\n # region pull_changes [C:4] [TYPE Function] [SEMANTICS git,pull,lock]\n # @PURPOSE: Pull changes from remote (concurrent-safe).\n # @PRE: Repository exists and has an 'origin' remote.\n # @POST: Changes from origin are pulled and merged into the active branch.\n def pull_changes(self, dashboard_id: int):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.pull_changes\"):\n repo = self.get_repo(dashboard_id)\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n if os.path.exists(merge_head_path):\n payload = self._build_unfinished_merge_payload(repo)\n logger.reason(\n f\"Unfinished merge detected for dashboard {dashboard_id} (repo_path={payload['repository_path']} git_dir={payload['git_dir']} branch={payload['current_branch']} merge_head={payload['merge_head']} merge_msg={payload['merge_message_preview']})\",\n extra={\"src\": \"pull_changes\"},\n )\n raise HTTPException(status_code=409, detail=payload)\n try:\n origin = repo.remote(name='origin')\n current_branch = repo.active_branch.name\n try:\n origin_urls = list(origin.urls)\n except Exception:\n origin_urls = []\n logger.reason(\n f\"Pull diagnostics dashboard={dashboard_id} repo_path={repo.working_tree_dir} branch={current_branch} origin_urls={origin_urls}\",\n extra={\"src\": \"pull_changes\"},\n )\n origin.fetch(prune=True)\n remote_ref = f\"origin/{current_branch}\"\n has_remote_branch = any(ref.name == remote_ref for ref in repo.refs)\n logger.reason(\n f\"Pull remote branch check dashboard={dashboard_id} branch={current_branch} remote_ref={remote_ref} exists={has_remote_branch}\",\n extra={\"src\": \"pull_changes\"},\n )\n if not has_remote_branch:\n raise HTTPException(\n status_code=409,\n detail=f\"Remote branch '{current_branch}' does not exist yet. Push this branch first.\",\n )\n logger.reason(f\"Pulling changes from origin/{current_branch}\", extra={\"src\": \"pull_changes\"})\n repo.git.pull(\"--no-rebase\", \"origin\", current_branch)\n except ValueError:\n logger.error(f\"[pull_changes][Coherence:Failed] Remote 'origin' not found for dashboard {dashboard_id}\")\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"conflict\" in lowered or \"not possible to fast-forward\" in lowered:\n raise HTTPException(\n status_code=409,\n detail=\"Pull requires conflict resolution. Resolve conflicts in repository and repeat operation.\",\n )\n logger.error(f\"[pull_changes][Coherence:Failed] Failed to pull changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git pull failed: {details}\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(f\"[pull_changes][Coherence:Failed] Failed to pull changes: {e}\")\n raise HTTPException(status_code=500, detail=f\"Git pull failed: {e!s}\")\n # endregion pull_changes\n# #endregion GitServiceSyncMixin\n# #endregion GitServiceSyncMixin\n" }, { "contract_id": "GitServiceUrlMixin", @@ -59708,10 +64210,10 @@ "file_path": "backend/src/services/mapping_service.py", "start_line": 1, "end_line": 91, - "tier": "TIER_2", - "complexity": 3, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]", "INVARIANT": "Suggestions are based on database names.", "LAYER": "Service", @@ -59734,56 +64236,10 @@ "target_ref": "suggest_mappings" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], + "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region mapping_service [C:3] [TYPE Module] [SEMANTICS mapping, database, superset, fuzzy, suggestion]\n#\n# @BRIEF Orchestrates database fetching and fuzzy matching suggestions.\n# @LAYER: Service\n# @PRE: source/target environment identifiers are provided by caller.\n# @POST: Exposes stateless mapping suggestion orchestration over configured environments.\n# @SIDE_EFFECT: Performs remote metadata reads through Superset API clients.\n# @DATA_CONTRACT: Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]\n# @RELATION DEPENDS_ON -> SupersetClient\n# @RELATION DEPENDS_ON -> suggest_mappings\n#\n# @INVARIANT: Suggestions are based on database names.\n\n\nfrom ..core.logger import belief_scope\nfrom ..core.superset_client import SupersetClient\nfrom ..core.utils.matching import suggest_mappings\n\n\n# #region MappingService [C:3] [TYPE Class]\n# @BRIEF Service for handling database mapping logic.\n# @PRE: config_manager exposes get_environments() with environment objects containing id.\n# @POST: Provides client resolution and mapping suggestion methods.\n# @SIDE_EFFECT: Instantiates Superset clients and performs upstream metadata reads.\n# @DATA_CONTRACT: Input[config_manager] -> Output[List[Dict]]\n# @RELATION DEPENDS_ON -> SupersetClient\n# @RELATION DEPENDS_ON -> suggest_mappings\nclass MappingService:\n # region init [TYPE Function]\n # @PURPOSE: Initializes the mapping service with a config manager.\n # @PRE: config_manager is provided.\n # @PARAM: config_manager (ConfigManager) - The configuration manager.\n # @POST: Service is initialized.\n # @RELATION: DEPENDS_ON -> MappingService\n def __init__(self, config_manager):\n with belief_scope(\"MappingService.__init__\"):\n self.config_manager = config_manager\n\n # endregion init\n\n # region _get_client [TYPE Function]\n # @PURPOSE: Helper to get an initialized SupersetClient for an environment.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PRE: environment must exist in config.\n # @POST: Returns an initialized SupersetClient.\n # @RETURN: SupersetClient - Initialized client.\n # @RELATION: CALLS -> SupersetClient\n def _get_client(self, env_id: str) -> SupersetClient:\n with belief_scope(\"MappingService._get_client\", f\"env_id={env_id}\"):\n envs = self.config_manager.get_environments()\n env = next((e for e in envs if e.id == env_id), None)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n\n return SupersetClient(env)\n\n # endregion _get_client\n\n # region get_suggestions [TYPE Function]\n # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.\n # @PARAM: source_env_id (str) - Source environment ID.\n # @PARAM: target_env_id (str) - Target environment ID.\n # @PRE: Both environments must be accessible.\n # @POST: Returns fuzzy-matched database suggestions.\n # @RETURN: List[Dict] - Suggested mappings.\n # @RELATION: CALLS -> _get_client\n # @RELATION: CALLS -> suggest_mappings\n async def get_suggestions(\n self, source_env_id: str, target_env_id: str\n ) -> list[dict]:\n with belief_scope(\n \"MappingService.get_suggestions\",\n f\"source={source_env_id}, target={target_env_id}\",\n ):\n \"\"\"\n Get suggested mappings between two environments.\n \"\"\"\n source_client = self._get_client(source_env_id)\n target_client = self._get_client(target_env_id)\n\n source_dbs = source_client.get_databases_summary()\n target_dbs = target_client.get_databases_summary()\n\n return suggest_mappings(source_dbs, target_dbs)\n\n # endregion get_suggestions\n\n\n# #endregion MappingService\n\n# #endregion mapping_service\n" + "body": "# #region mapping_service [C:5] [TYPE Module] [SEMANTICS mapping, database, superset, fuzzy, suggestion]\n#\n# @BRIEF Orchestrates database fetching and fuzzy matching suggestions.\n# @LAYER: Service\n# @PRE: source/target environment identifiers are provided by caller.\n# @POST: Exposes stateless mapping suggestion orchestration over configured environments.\n# @SIDE_EFFECT: Performs remote metadata reads through Superset API clients.\n# @DATA_CONTRACT: Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]\n# @RELATION DEPENDS_ON -> SupersetClient\n# @RELATION DEPENDS_ON -> suggest_mappings\n#\n# @INVARIANT: Suggestions are based on database names.\n\n\nfrom ..core.logger import belief_scope\nfrom ..core.superset_client import SupersetClient\nfrom ..core.utils.matching import suggest_mappings\n\n\n# #region MappingService [C:3] [TYPE Class]\n# @BRIEF Service for handling database mapping logic.\n# @PRE: config_manager exposes get_environments() with environment objects containing id.\n# @POST: Provides client resolution and mapping suggestion methods.\n# @SIDE_EFFECT: Instantiates Superset clients and performs upstream metadata reads.\n# @DATA_CONTRACT: Input[config_manager] -> Output[List[Dict]]\n# @RELATION DEPENDS_ON -> SupersetClient\n# @RELATION DEPENDS_ON -> suggest_mappings\nclass MappingService:\n # region init [TYPE Function]\n # @PURPOSE: Initializes the mapping service with a config manager.\n # @PRE: config_manager is provided.\n # @PARAM: config_manager (ConfigManager) - The configuration manager.\n # @POST: Service is initialized.\n # @RELATION: DEPENDS_ON -> MappingService\n def __init__(self, config_manager):\n with belief_scope(\"MappingService.__init__\"):\n self.config_manager = config_manager\n\n # endregion init\n\n # region _get_client [TYPE Function]\n # @PURPOSE: Helper to get an initialized SupersetClient for an environment.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PRE: environment must exist in config.\n # @POST: Returns an initialized SupersetClient.\n # @RETURN: SupersetClient - Initialized client.\n # @RELATION: CALLS -> SupersetClient\n def _get_client(self, env_id: str) -> SupersetClient:\n with belief_scope(\"MappingService._get_client\", f\"env_id={env_id}\"):\n envs = self.config_manager.get_environments()\n env = next((e for e in envs if e.id == env_id), None)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n\n return SupersetClient(env)\n\n # endregion _get_client\n\n # region get_suggestions [TYPE Function]\n # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.\n # @PARAM: source_env_id (str) - Source environment ID.\n # @PARAM: target_env_id (str) - Target environment ID.\n # @PRE: Both environments must be accessible.\n # @POST: Returns fuzzy-matched database suggestions.\n # @RETURN: List[Dict] - Suggested mappings.\n # @RELATION: CALLS -> _get_client\n # @RELATION: CALLS -> suggest_mappings\n async def get_suggestions(\n self, source_env_id: str, target_env_id: str\n ) -> list[dict]:\n with belief_scope(\n \"MappingService.get_suggestions\",\n f\"source={source_env_id}, target={target_env_id}\",\n ):\n \"\"\"\n Get suggested mappings between two environments.\n \"\"\"\n source_client = self._get_client(source_env_id)\n target_client = self._get_client(target_env_id)\n\n source_dbs = source_client.get_databases_summary()\n target_dbs = target_client.get_databases_summary()\n\n return suggest_mappings(source_dbs, target_dbs)\n\n # endregion get_suggestions\n\n\n# #endregion MappingService\n\n# #endregion mapping_service\n" }, { "contract_id": "MappingService", @@ -60328,123 +64784,378 @@ "body": "# #region NotificationService [C:4] [TYPE Class]\n# @BRIEF Routes validation reports to appropriate users and channels.\n# @RELATION DEPENDS_ON -> [NotificationProvider]\n# @RELATION DEPENDS_ON -> [ValidationRecord]\n# @RELATION DEPENDS_ON -> [ValidationPolicy]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @PRE: Service receives a live DB session and configuration manager with notification payload settings.\n# @POST: Service can resolve targets and dispatch provider sends without mutating validation records.\n# @SIDE_EFFECT: Reads notification configuration, queries user preferences, and dispatches provider I/O.\n# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]\nclass NotificationService:\n # region NotificationService_init [TYPE Function]\n # @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing.\n # @RELATION: [BINDS_TO] ->[NotificationService]\n # @RELATION: [DEPENDS_ON] ->[ValidationPolicy]\n def __init__(self, db: Session, config_manager: ConfigManager):\n self.db = db\n self.config_manager = config_manager\n self._providers: dict[str, NotificationProvider] = {}\n self._initialized = False\n\n # endregion NotificationService_init\n\n # region _initialize_providers [TYPE Function]\n # @PURPOSE: Materialize configured notification channel adapters once per service lifetime.\n # @RELATION: [DEPENDS_ON] ->[SMTPProvider]\n # @RELATION: [DEPENDS_ON] ->[TelegramProvider]\n # @RELATION: [DEPENDS_ON] ->[SlackProvider]\n def _initialize_providers(self):\n if self._initialized:\n return\n\n # In a real implementation, we would fetch these from NotificationConfig model\n # For now, we'll use a placeholder initialization logic\n # T033 will implement the UI/API for this.\n configs = self.config_manager.get_payload().get(\"notifications\", {})\n\n if \"smtp\" in configs:\n self._providers[\"SMTP\"] = SMTPProvider(configs[\"smtp\"])\n if \"telegram\" in configs:\n self._providers[\"TELEGRAM\"] = TelegramProvider(configs[\"telegram\"])\n if \"slack\" in configs:\n self._providers[\"SLACK\"] = SlackProvider(configs[\"slack\"])\n\n self._initialized = True\n\n # endregion _initialize_providers\n\n # region dispatch_report [TYPE Function]\n # @PURPOSE: Route one validation record to resolved owners and configured custom channels.\n # @RELATION: [CALLS] ->[_initialize_providers]\n # @RELATION: [CALLS] ->[_should_notify]\n # @RELATION: [CALLS] ->[_resolve_targets]\n # @RELATION: [CALLS] ->[_build_body]\n # @PRE: record is persisted and providers can be initialized from configuration payload.\n # @POST: Eligible notification sends are scheduled in background or awaited inline.\n # @SIDE_EFFECT: Schedules or performs outbound provider sends and emits notification logs.\n # @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]\n async def dispatch_report(\n self,\n record: ValidationRecord,\n policy: ValidationPolicy | None = None,\n background_tasks: BackgroundTasks | None = None,\n ):\n \"\"\"\n Route a validation record to owners and custom channels.\n @PRE: record is persisted.\n @POST: Dispatches async tasks for each resolved target.\n \"\"\"\n with belief_scope(\n \"NotificationService.dispatch_report\", f\"record_id={record.id}\"\n ):\n self._initialize_providers()\n\n # 1. Determine if we should notify based on status and policy\n should_notify = self._should_notify(record, policy)\n if not should_notify:\n logger.reason(\n f\"Notification skipped for record {record.id} (status={record.status})\",\n extra={\"src\": \"NotificationService\"},\n )\n return\n\n # 2. Resolve targets (Owners + Custom Channels)\n targets = self._resolve_targets(record, policy)\n\n # 3. Dispatch\n subject = f\"Dashboard Health Alert: {record.status}\"\n body = self._build_body(record)\n\n for channel_type, recipient in targets:\n provider = self._providers.get(channel_type)\n if not provider:\n logger.explore(\n f\"Unsupported or unconfigured channel: {channel_type}\",\n extra={\"src\": \"NotificationService\"},\n )\n continue\n\n if background_tasks:\n background_tasks.add_task(provider.send, recipient, subject, body)\n else:\n # Fallback to sync for tests or if no background_tasks provided\n await provider.send(recipient, subject, body)\n\n # endregion dispatch_report\n\n # region _should_notify [TYPE Function]\n # @PURPOSE: Evaluate record status against effective alert policy.\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ValidationPolicy]\n def _should_notify(\n self, record: ValidationRecord, policy: ValidationPolicy | None\n ) -> bool:\n condition = policy.alert_condition if policy else \"FAIL_ONLY\"\n\n if condition == \"ALWAYS\":\n return True\n if condition == \"WARN_AND_FAIL\":\n return record.status in (\"WARN\", \"FAIL\")\n return record.status == \"FAIL\"\n\n # endregion _should_notify\n\n # region _resolve_targets [TYPE Function]\n # @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record.\n # @RELATION: [CALLS] ->[_find_dashboard_owners]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ValidationPolicy]\n def _resolve_targets(\n self, record: ValidationRecord, policy: ValidationPolicy | None\n ) -> list[tuple]:\n targets = []\n\n # Owner routing\n if not policy or policy.notify_owners:\n owners = self._find_dashboard_owners(record)\n for owner_pref in owners:\n if not owner_pref.notify_on_fail:\n continue\n\n if owner_pref.telegram_id:\n targets.append((\"TELEGRAM\", owner_pref.telegram_id))\n\n email = owner_pref.email_address or getattr(\n owner_pref.user, \"email\", None\n )\n if email:\n targets.append((\"SMTP\", email))\n\n # Custom channels from policy\n if policy and policy.custom_channels:\n for channel in policy.custom_channels:\n # channel format: {\"type\": \"SLACK\", \"target\": \"#alerts\"}\n targets.append((channel.get(\"type\"), channel.get(\"target\")))\n\n return targets\n\n # endregion _resolve_targets\n\n # region _find_dashboard_owners [TYPE Function]\n # @PURPOSE: Load candidate dashboard owners from persisted profile preferences.\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[UserDashboardPreference]\n def _find_dashboard_owners(\n self, record: ValidationRecord\n ) -> list[UserDashboardPreference]:\n # This is a simplified owner lookup.\n # In a real scenario, we'd query Superset for owners, then match them to our UserDashboardPreference.\n # For now, we'll return all users who have bound this dashboard's environment and have a username.\n\n # Placeholder: return all preferences that have a superset_username\n # (In production, we'd filter by actual ownership from Superset metadata)\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.superset_username != None)\n .all()\n )\n\n # endregion _find_dashboard_owners\n\n # region _build_body [TYPE Function]\n # @PURPOSE: Format one validation record into provider-ready body text.\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n def _build_body(self, record: ValidationRecord) -> str:\n return (\n f\"Dashboard ID: {record.dashboard_id}\\n\"\n f\"Environment: {record.environment_id}\\n\"\n f\"Status: {record.status}\\n\\n\"\n f\"Summary: {record.summary}\\n\\n\"\n f\"Issues found: {len(record.issues)}\"\n )\n\n # endregion _build_body\n\n\n# #endregion NotificationService\n" }, { - "contract_id": "profile_service", + "contract_id": "profile_preference_service", "contract_type": "Module", - "file_path": "backend/src/services/profile_service.py", + "file_path": "backend/src/services/profile_preference_service.py", "start_line": 1, - "end_line": 857, - "tier": "TIER_3", - "complexity": 5, + "end_line": 359, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Profile_id -> ProfileInfo; session_id -> valid UUID", - "INVARIANT": "Profile ID needs to unique per-user session", - "LAYER": "Domain", - "POST": "Profile with updated fields populated and", - "PRE": "Session is active and valid", - "PURPOSE": "Orchestrates profile preference persistence, Superset account lookup, and deterministic actor matching.", - "SIDE_EFFECT": "Database read/write operations", - "TEST_CONTRACT": "ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse", - "TEST_EDGE": "lookup_env_not_found -> unknown environment_id returns not found", - "TEST_FIXTURE": "valid_profile_update -> {\"user_id\":\"u-1\",\"superset_username\":\"John_Doe\",\"show_only_my_dashboards\":true}", - "TEST_INVARIANT": "normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]" + "COMPLEXITY": 4, + "PURPOSE": "Profile preference persistence — read, update, and normalize user dashboard filter preferences" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], + "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_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_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_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" + }, + { + "contract_id": "ProfilePreferenceService", + "contract_type": "Class", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 50, + "end_line": 358, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Preference CRUD operations are user-scoped and return normalized ProfilePreferenceResponse.", + "PRE": "db session is active.", + "PURPOSE": "Handles profile preference persistence, validation, and token encryption." }, "relations": [ { - "source_id": "profile_service", + "source_id": "ProfilePreferenceService", + "relation_type": "DEPENDS_ON", + "target_id": "AuthRepository", + "target_ref": "[AuthRepository]" + }, + { + "source_id": "ProfilePreferenceService", + "relation_type": "DEPENDS_ON", + "target_id": "EncryptionManager", + "target_ref": "[EncryptionManager]" + }, + { + "source_id": "ProfilePreferenceService", "relation_type": "DEPENDS_ON", "target_id": "UserDashboardPreference", "target_ref": "[UserDashboardPreference]" - }, - { - "source_id": "profile_service", - "relation_type": "DEPENDS_ON", - "target_id": "ProfilePreferenceResponse", - "target_ref": "[ProfilePreferenceResponse]" - }, - { - "source_id": "profile_service", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" - }, - { - "source_id": "profile_service", - "relation_type": "DEPENDS_ON", - "target_id": "AuthRepositoryModule", - "target_ref": "[AuthRepositoryModule]" - }, - { - "source_id": "profile_service", - "relation_type": "DEPENDS_ON", - "target_id": "User", - "target_ref": "[User]" - }, - { - "source_id": "profile_service", - "relation_type": "DEPENDS_ON", - "target_id": "sqlalchemy.orm.Session", - "target_ref": "[sqlalchemy.orm.Session]" } ], "schema_warnings": [ { - "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C4", "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] + "actual_complexity": 4, + "contract_type": "Class" + } + } + ], + "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_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" + }, + { + "contract_id": "get_my_preference", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 73, + "end_line": 100, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returned payload belongs to current_user only.", + "PRE": "current_user is authenticated.", + "PURPOSE": "Return current user's persisted preference or default non-configured view." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C5", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "get_dashboard_filter_binding", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 102, + "end_line": 133, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returns normalized username and profile-default filter toggles without security summary expansion.", + "PRE": "current_user is authenticated.", + "PURPOSE": "Return only dashboard-filter fields required by dashboards listing hot path." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "update_my_preference", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 135, + "end_line": 278, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Preference row for current_user is created/updated when validation passes.", + "PRE": "current_user is authenticated and payload is provided.", + "PURPOSE": "Validate and persist current user's profile preference in self-scoped mode." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.utcnow()\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n" + }, + { + "contract_id": "_to_preference_payload", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 280, + "end_line": 336, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returns normalized ProfilePreference object.", + "PRE": "preference row can contain nullable optional fields.", + "PURPOSE": "Map ORM preference row to API DTO with token metadata." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.utcnow()\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n" + }, + { + "contract_id": "_build_default_preference", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 338, + "end_line": 342, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Delegate to profile_utils.build_default_preference." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #region _build_default_preference [C:1] [TYPE Function]\n # @BRIEF Delegate to profile_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" + }, + { + "contract_id": "_get_preference_row", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 344, + "end_line": 348, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Return persisted preference row for user or None." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "_get_or_create_preference_row", + "contract_type": "Function", + "file_path": "backend/src/services/profile_preference_service.py", + "start_line": 350, + "end_line": 357, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Return existing preference row or create new unsaved row." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "profile_service", + "contract_type": "Module", + "file_path": "backend/src/services/profile_service.py", + "start_line": 1, + "end_line": 172, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "PURPOSE": "Composite facade orchestrating profile preference persistence, Superset account lookup," + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", "detail": { "actual_complexity": 5, "contract_type": "Module" } }, { - "code": "tag_not_for_contract_type", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is forbidden for contract type 'Module' at C5", + "code": "missing_required_tag", + "tag": "INVARIANT", + "message": "@INVARIANT is required for contract type 'Module' at C5", "detail": { "actual_complexity": 5, "contract_type": "Module" } }, { - "code": "tag_not_for_contract_type", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is not allowed for contract type 'Module'", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_type": "Module", - "allowed_types": [ - "Block" - ] + "actual_complexity": 5, + "contract_type": "Module" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is forbidden for contract type 'Module' at C5", + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", "detail": { "actual_complexity": 5, "contract_type": "Module" @@ -60453,14 +65164,87 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region profile_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, preference, superset, lookup]\n#\n# @BRIEF Orchestrates profile preference persistence, Superset account lookup, and deterministic actor matching.\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 -> [sqlalchemy.orm.Session]\n#\n# @INVARIANT: Profile ID needs to 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\nfrom collections.abc import Iterable, Sequence\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 ..core.superset_client import SupersetClient\nfrom ..core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom ..models.auth import User\nfrom ..models.profile import UserDashboardPreference\nfrom ..schemas.profile import (\n ProfilePermissionState,\n ProfilePreference,\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n ProfileSecuritySummary,\n SupersetAccountCandidate,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom .llm_provider import EncryptionManager\nfrom .rbac_permission_catalog import discover_declared_permissions\n\nSUPPORTED_START_PAGES = {\"dashboards\", \"datasets\", \"reports\"}\nSUPPORTED_DENSITIES = {\"compact\", \"comfortable\"}\n\n\n# #region ProfileValidationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> 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\n\n# #endregion ProfileValidationError\n\n\n# #region EnvironmentNotFoundError [C:2] [TYPE Class]\n# @RELATION INHERITS -> Exception\n# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n\n\n# #endregion EnvironmentNotFoundError\n\n\n# #region ProfileAuthorizationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> Exception\n# @BRIEF Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n\n\n# #endregion ProfileAuthorizationError\n\n\n# #region ProfileService [C:5] [TYPE Class]\n# @RELATION DEPENDS_ON -> [sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @RELATION CALLS -> [discover_declared_permissions]\n# @BRIEF Implements profile preference read/update flow and Superset account lookup degradation strategy.\n# @PRE: Caller provides authenticated User context for external service methods.\n# @POST: Preference operations remain user-scoped and return 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\nclass ProfileService:\n # region init [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Initialize service with DB session and config manager.\n # @PRE: db session is active and config_manager supports get_environments().\n # @POST: Service is ready for preference persistence and lookup 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\n # endregion init\n\n # region get_my_preference [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 \"ProfileService.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._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=self._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\n # endregion get_my_preference\n\n # region get_dashboard_filter_binding [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 \"ProfileService.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\": self._sanitize_username(\n preference.superset_username\n ),\n \"superset_username_normalized\": self._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\n # endregion get_dashboard_filter_binding\n\n # region update_my_preference [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 \"ProfileService.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 = self._sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = self._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 = self._sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = self._sanitize_text(payload.git_username)\n\n effective_git_email = self._sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = self._sanitize_text(payload.git_email)\n\n effective_start_page = self._normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = self._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 = self._normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = self._normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = self._sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = self._sanitize_text(payload.telegram_id)\n\n effective_email_address = self._sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = self._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 = self._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 = self._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 = self._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._build_security_summary(current_user),\n )\n\n # endregion update_my_preference\n\n # region lookup_superset_accounts [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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: User,\n request: SupersetAccountLookupRequest,\n ) -> SupersetAccountLookupResponse:\n with belief_scope(\n \"ProfileService.lookup_superset_accounts\",\n f\"user_id={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\n # endregion lookup_superset_accounts\n\n # region matches_dashboard_actor [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 = self._normalize_username(bound_username)\n if not normalized_actor:\n return False\n\n owner_tokens = self._normalize_owner_tokens(owners)\n modified_token = self._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 # region _build_security_summary [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 role_names_set: set[str] = set()\n roles = getattr(current_user, \"roles\", []) or []\n for role in roles:\n normalized_role_name = self._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 = self._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\": \"ProfileService._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 = self._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\n # endregion _build_security_summary\n\n # region _collect_user_permission_pairs [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 = self._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\n # endregion _collect_user_permission_pairs\n\n # region _format_permission_key [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Convert normalized permission pair to compact UI key.\n # @PRE: resource and action are normalized.\n # @POST: Returns user-facing badge key.\n def _format_permission_key(self, resource: str, action: str) -> str:\n normalized_resource = self._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\n # endregion _format_permission_key\n\n # region _to_preference_payload [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 = self._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 = self._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=self._sanitize_username(preference.superset_username),\n superset_username_normalized=self._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=self._sanitize_text(preference.git_username),\n git_email=self._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=self._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=self._normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=self._sanitize_text(preference.telegram_id),\n email_address=self._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\n # endregion _to_preference_payload\n\n # region _mask_secret_value [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Build a safe display value for sensitive secrets.\n # @PRE: secret may be None or plaintext.\n # @POST: Returns masked representation or None.\n def _mask_secret_value(self, secret: str | None) -> str | None:\n sanitized_secret = self._sanitize_secret(secret)\n if sanitized_secret is None:\n return None\n if len(sanitized_secret) <= 4:\n return \"***\"\n return f\"{sanitized_secret[:2]}***{sanitized_secret[-2:]}\"\n\n # endregion _mask_secret_value\n\n # region _sanitize_text [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize optional text into trimmed form or None.\n # @PRE: value may be empty or None.\n # @POST: Returns trimmed value or None.\n def _sanitize_text(self, value: str | None) -> str | None:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n\n # endregion _sanitize_text\n\n # region _sanitize_secret [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize secret input into trimmed form or None.\n # @PRE: value may be None or blank.\n # @POST: Returns trimmed secret or None.\n def _sanitize_secret(self, 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\n # endregion _sanitize_secret\n\n # region _normalize_start_page [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize supported start page aliases to canonical values.\n # @PRE: value may be None or alias.\n # @POST: Returns one of SUPPORTED_START_PAGES.\n def _normalize_start_page(self, 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\n # endregion _normalize_start_page\n\n # region _normalize_density [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize supported density aliases to canonical values.\n # @PRE: value may be None or alias.\n # @POST: Returns one of SUPPORTED_DENSITIES.\n def _normalize_density(self, 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\n # endregion _normalize_density\n\n # region _resolve_environment [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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\n # endregion _resolve_environment\n\n # region _get_preference_row [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Return persisted preference row for user or None.\n # @PRE: user_id is provided.\n # @POST: Returns matching row 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\n # endregion _get_preference_row\n\n # region _get_or_create_preference_row [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Return existing preference row or create new unsaved row.\n # @PRE: user_id is provided.\n # @POST: Returned row always contains user_id.\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\n # endregion _get_or_create_preference_row\n\n # region _build_default_preference [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Build non-persisted default preference DTO for unconfigured users.\n # @PRE: user_id is provided.\n # @POST: Returns ProfilePreference with disabled toggle and empty username.\n def _build_default_preference(self, user_id: str) -> 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\n # endregion _build_default_preference\n\n # region _validate_update_payload [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Validate username/toggle constraints for preference mutation.\n # @PRE: payload is provided.\n # @POST: Returns validation errors list; empty list means valid.\n def _validate_update_payload(\n self,\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 = self._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 = self._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 = self._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\n # endregion _validate_update_payload\n\n # region _sanitize_username [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize raw username into trimmed form or None for empty input.\n # @PRE: value can be empty or None.\n # @POST: Returns trimmed username or None.\n def _sanitize_username(self, value: str | None) -> str | None:\n return self._sanitize_text(value)\n\n # endregion _sanitize_username\n\n # region _normalize_username [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Apply deterministic trim+lower normalization for actor matching.\n # @PRE: value can be empty or None.\n # @POST: Returns lowercase normalized token or None.\n def _normalize_username(self, value: str | None) -> str | None:\n sanitized = self._sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n\n # endregion _normalize_username\n\n # region _normalize_owner_tokens [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize owners payload into deduplicated lower-cased tokens.\n # @PRE: owners can be iterable of scalars or dict-like values.\n # @POST: Returns list of unique normalized owner tokens.\n def _normalize_owner_tokens(self, 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 = self._sanitize_username(str(owner.get(\"first_name\") or \"\"))\n last_name = self._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 = self._normalize_username(str(candidate or \"\"))\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n\n # endregion _normalize_owner_tokens\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 -> [sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [ProfilePreferenceService]\n# @RELATION DEPENDS_ON -> [SupersetLookupService]\n# @RELATION DEPENDS_ON -> [SecurityBadgeService]\n# @RELATION DEPENDS_ON -> [profile_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_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_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_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" + }, + { + "contract_id": "ProfileService", + "contract_type": "Class", + "file_path": "backend/src/services/profile_service.py", + "start_line": 74, + "end_line": 170, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "Facade that composes ProfilePreferenceService, SupersetLookupService, and" + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + } + ], + "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_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" + }, + { + "contract_id": "profile_utils", + "contract_type": "Module", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 1, + "end_line": 244, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Pure utility helpers for profile data sanitization, normalization, and secret masking." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region profile_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 -> 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 -> 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 -> 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 -> [SUPPORTED_DENSITIES, 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_utils\n" }, { "contract_id": "ProfileValidationError", "contract_type": "Class", - "file_path": "backend/src/services/profile_service.py", - "start_line": 54, - "end_line": 63, + "file_path": "backend/src/services/profile_utils.py", + "start_line": 17, + "end_line": 24, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -60488,14 +65272,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileValidationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> 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\n\n# #endregion ProfileValidationError\n" + "body": "# #region ProfileValidationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> 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" }, { "contract_id": "EnvironmentNotFoundError", "contract_type": "Class", - "file_path": "backend/src/services/profile_service.py", - "start_line": 66, - "end_line": 73, + "file_path": "backend/src/services/profile_utils.py", + "start_line": 27, + "end_line": 32, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -60523,14 +65307,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region EnvironmentNotFoundError [C:2] [TYPE Class]\n# @RELATION INHERITS -> Exception\n# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n\n\n# #endregion EnvironmentNotFoundError\n" + "body": "# #region EnvironmentNotFoundError [C:2] [TYPE Class]\n# @RELATION INHERITS -> Exception\n# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n# #endregion EnvironmentNotFoundError\n" }, { "contract_id": "ProfileAuthorizationError", "contract_type": "Class", - "file_path": "backend/src/services/profile_service.py", - "start_line": 76, - "end_line": 83, + "file_path": "backend/src/services/profile_utils.py", + "start_line": 35, + "end_line": 40, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -60558,67 +65342,260 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileAuthorizationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> Exception\n# @BRIEF Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n\n\n# #endregion ProfileAuthorizationError\n" + "body": "# #region ProfileAuthorizationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> Exception\n# @BRIEF Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n# #endregion ProfileAuthorizationError\n" }, { - "contract_id": "ProfileService", - "contract_type": "Class", - "file_path": "backend/src/services/profile_service.py", - "start_line": 86, - "end_line": 855, - "tier": "TIER_3", - "complexity": 5, + "contract_id": "sanitize_text", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 46, + "end_line": 53, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]", - "INVARIANT": "Profile data integrity maintained, cache consistency with database state", - "POST": "Preference operations remain user-scoped and return normalized profile/lookup responses.", - "PRE": "Caller provides authenticated User context for external service methods.", - "PURPOSE": "Implements profile preference read/update flow and Superset account lookup degradation strategy.", - "SIDE_EFFECT": "Writes preference records and encrypted tokens; performs external account lookups when requested." + "COMPLEXITY": 1, + "PURPOSE": "Normalize optional text into trimmed form or None." }, - "relations": [ - { - "source_id": "ProfileService", - "relation_type": "DEPENDS_ON", - "target_id": "sqlalchemy.orm.Session", - "target_ref": "[sqlalchemy.orm.Session]" - }, - { - "source_id": "ProfileService", - "relation_type": "DEPENDS_ON", - "target_id": "AuthRepository", - "target_ref": "[AuthRepository]" - }, - { - "source_id": "ProfileService", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" - }, - { - "source_id": "ProfileService", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetAccountLookupAdapter", - "target_ref": "[SupersetAccountLookupAdapter]" - }, - { - "source_id": "ProfileService", - "relation_type": "DEPENDS_ON", - "target_id": "UserDashboardPreference", - "target_ref": "[UserDashboardPreference]" - }, - { - "source_id": "ProfileService", - "relation_type": "CALLS", - "target_id": "discover_declared_permissions", - "target_ref": "[discover_declared_permissions]" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileService [C:5] [TYPE Class]\n# @RELATION DEPENDS_ON -> [sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @RELATION CALLS -> [discover_declared_permissions]\n# @BRIEF Implements profile preference read/update flow and Superset account lookup degradation strategy.\n# @PRE: Caller provides authenticated User context for external service methods.\n# @POST: Preference operations remain user-scoped and return 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\nclass ProfileService:\n # region init [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Initialize service with DB session and config manager.\n # @PRE: db session is active and config_manager supports get_environments().\n # @POST: Service is ready for preference persistence and lookup 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\n # endregion init\n\n # region get_my_preference [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 \"ProfileService.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._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=self._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\n # endregion get_my_preference\n\n # region get_dashboard_filter_binding [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 \"ProfileService.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\": self._sanitize_username(\n preference.superset_username\n ),\n \"superset_username_normalized\": self._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\n # endregion get_dashboard_filter_binding\n\n # region update_my_preference [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 \"ProfileService.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 = self._sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = self._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 = self._sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = self._sanitize_text(payload.git_username)\n\n effective_git_email = self._sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = self._sanitize_text(payload.git_email)\n\n effective_start_page = self._normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = self._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 = self._normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = self._normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = self._sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = self._sanitize_text(payload.telegram_id)\n\n effective_email_address = self._sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = self._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 = self._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 = self._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 = self._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._build_security_summary(current_user),\n )\n\n # endregion update_my_preference\n\n # region lookup_superset_accounts [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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: User,\n request: SupersetAccountLookupRequest,\n ) -> SupersetAccountLookupResponse:\n with belief_scope(\n \"ProfileService.lookup_superset_accounts\",\n f\"user_id={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\n # endregion lookup_superset_accounts\n\n # region matches_dashboard_actor [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 = self._normalize_username(bound_username)\n if not normalized_actor:\n return False\n\n owner_tokens = self._normalize_owner_tokens(owners)\n modified_token = self._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 # region _build_security_summary [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 role_names_set: set[str] = set()\n roles = getattr(current_user, \"roles\", []) or []\n for role in roles:\n normalized_role_name = self._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 = self._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\": \"ProfileService._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 = self._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\n # endregion _build_security_summary\n\n # region _collect_user_permission_pairs [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 = self._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\n # endregion _collect_user_permission_pairs\n\n # region _format_permission_key [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Convert normalized permission pair to compact UI key.\n # @PRE: resource and action are normalized.\n # @POST: Returns user-facing badge key.\n def _format_permission_key(self, resource: str, action: str) -> str:\n normalized_resource = self._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\n # endregion _format_permission_key\n\n # region _to_preference_payload [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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 = self._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 = self._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=self._sanitize_username(preference.superset_username),\n superset_username_normalized=self._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=self._sanitize_text(preference.git_username),\n git_email=self._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=self._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=self._normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=self._sanitize_text(preference.telegram_id),\n email_address=self._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\n # endregion _to_preference_payload\n\n # region _mask_secret_value [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Build a safe display value for sensitive secrets.\n # @PRE: secret may be None or plaintext.\n # @POST: Returns masked representation or None.\n def _mask_secret_value(self, secret: str | None) -> str | None:\n sanitized_secret = self._sanitize_secret(secret)\n if sanitized_secret is None:\n return None\n if len(sanitized_secret) <= 4:\n return \"***\"\n return f\"{sanitized_secret[:2]}***{sanitized_secret[-2:]}\"\n\n # endregion _mask_secret_value\n\n # region _sanitize_text [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize optional text into trimmed form or None.\n # @PRE: value may be empty or None.\n # @POST: Returns trimmed value or None.\n def _sanitize_text(self, value: str | None) -> str | None:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n\n # endregion _sanitize_text\n\n # region _sanitize_secret [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize secret input into trimmed form or None.\n # @PRE: value may be None or blank.\n # @POST: Returns trimmed secret or None.\n def _sanitize_secret(self, 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\n # endregion _sanitize_secret\n\n # region _normalize_start_page [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize supported start page aliases to canonical values.\n # @PRE: value may be None or alias.\n # @POST: Returns one of SUPPORTED_START_PAGES.\n def _normalize_start_page(self, 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\n # endregion _normalize_start_page\n\n # region _normalize_density [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize supported density aliases to canonical values.\n # @PRE: value may be None or alias.\n # @POST: Returns one of SUPPORTED_DENSITIES.\n def _normalize_density(self, 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\n # endregion _normalize_density\n\n # region _resolve_environment [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: 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\n # endregion _resolve_environment\n\n # region _get_preference_row [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Return persisted preference row for user or None.\n # @PRE: user_id is provided.\n # @POST: Returns matching row 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\n # endregion _get_preference_row\n\n # region _get_or_create_preference_row [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Return existing preference row or create new unsaved row.\n # @PRE: user_id is provided.\n # @POST: Returned row always contains user_id.\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\n # endregion _get_or_create_preference_row\n\n # region _build_default_preference [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Build non-persisted default preference DTO for unconfigured users.\n # @PRE: user_id is provided.\n # @POST: Returns ProfilePreference with disabled toggle and empty username.\n def _build_default_preference(self, user_id: str) -> 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\n # endregion _build_default_preference\n\n # region _validate_update_payload [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Validate username/toggle constraints for preference mutation.\n # @PRE: payload is provided.\n # @POST: Returns validation errors list; empty list means valid.\n def _validate_update_payload(\n self,\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 = self._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 = self._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 = self._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\n # endregion _validate_update_payload\n\n # region _sanitize_username [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize raw username into trimmed form or None for empty input.\n # @PRE: value can be empty or None.\n # @POST: Returns trimmed username or None.\n def _sanitize_username(self, value: str | None) -> str | None:\n return self._sanitize_text(value)\n\n # endregion _sanitize_username\n\n # region _normalize_username [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Apply deterministic trim+lower normalization for actor matching.\n # @PRE: value can be empty or None.\n # @POST: Returns lowercase normalized token or None.\n def _normalize_username(self, value: str | None) -> str | None:\n sanitized = self._sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n\n # endregion _normalize_username\n\n # region _normalize_owner_tokens [TYPE Function]\n # @RELATION: BINDS_TO -> ProfileService\n # @PURPOSE: Normalize owners payload into deduplicated lower-cased tokens.\n # @PRE: owners can be iterable of scalars or dict-like values.\n # @POST: Returns list of unique normalized owner tokens.\n def _normalize_owner_tokens(self, 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 = self._sanitize_username(str(owner.get(\"first_name\") or \"\"))\n last_name = self._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 = self._normalize_username(str(candidate or \"\"))\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n\n # endregion _normalize_owner_tokens\n\n\n# #endregion ProfileService\n" + "body": "# #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" + }, + { + "contract_id": "sanitize_secret", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 56, + "end_line": 65, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Normalize secret input into trimmed form or None." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "sanitize_username", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 68, + "end_line": 72, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Normalize raw username into trimmed form or None for empty input." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "normalize_username", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 75, + "end_line": 82, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Apply deterministic trim+lower normalization for actor matching." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "normalize_start_page", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 85, + "end_line": 94, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Normalize supported start page aliases to canonical values." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "normalize_density", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 97, + "end_line": 106, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Normalize supported density aliases to canonical values." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "mask_secret_value", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 109, + "end_line": 118, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Build a safe display value for sensitive secrets." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "validate_update_payload", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 121, + "end_line": 174, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Returns validation errors list; empty list means valid.", + "PURPOSE": "Validate username/toggle constraints for preference mutation." + }, + "relations": [ + { + "source_id": "validate_update_payload", + "relation_type": "CALLS", + "target_id": "sanitize_username", + "target_ref": "[sanitize_username]" + }, + { + "source_id": "validate_update_payload", + "relation_type": "CALLS", + "target_id": "sanitize_text", + "target_ref": "[sanitize_text]" + }, + { + "source_id": "validate_update_payload", + "relation_type": "DEPENDS_ON", + "target_id": "SUPPORTED_DENSITIES, SUPPORTED_START_PAGES", + "target_ref": "[SUPPORTED_DENSITIES, SUPPORTED_START_PAGES]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "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 -> [SUPPORTED_DENSITIES, 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" + }, + { + "contract_id": "build_default_preference", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 177, + "end_line": 203, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Build non-persisted default preference DTO for unconfigured users." + }, + "relations": [ + { + "source_id": "build_default_preference", + "relation_type": "DEPENDS_ON", + "target_id": "ProfilePreference", + "target_ref": "[ProfilePreference]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region build_default_preference [C:2] [TYPE Function] [SEMANTICS preference,default,unconfigured]\n# @BRIEF Build non-persisted default preference DTO for unconfigured users.\n# @RELATION DEPENDS_ON -> [ProfilePreference]\ndef build_default_preference(user_id: str) -> Any:\n \"\"\"Build default ProfilePreference with disabled toggles and empty usernames.\"\"\"\n from ..schemas.profile import ProfilePreference\n now = datetime.utcnow()\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=None,\n superset_username_normalized=None,\n show_only_my_dashboards=False,\n show_only_slug_dashboards=True,\n git_username=None,\n git_email=None,\n has_git_personal_access_token=False,\n git_personal_access_token_masked=None,\n start_page=\"dashboards\",\n auto_open_task_drawer=True,\n dashboards_table_density=\"comfortable\",\n telegram_id=None,\n email_address=None,\n notify_on_fail=True,\n created_at=now,\n updated_at=now,\n )\n# #endregion build_default_preference\n" + }, + { + "contract_id": "normalize_owner_tokens", + "contract_type": "Function", + "file_path": "backend/src/services/profile_utils.py", + "start_line": 206, + "end_line": 243, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Normalize owners payload into deduplicated lower-cased tokens." + }, + "relations": [ + { + "source_id": "normalize_owner_tokens", + "relation_type": "CALLS", + "target_id": "normalize_username", + "target_ref": "[normalize_username]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" }, { "contract_id": "rbac_permission_catalog", @@ -61568,14 +66545,16 @@ "contract_type": "Module", "file_path": "backend/src/services/resource_service.py", "start_line": 1, - "end_line": 482, - "tier": "TIER_2", - "complexity": 3, + "end_line": 484, + "tier": "TIER_3", + "complexity": 5, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 5, + "DATA_CONTRACT": "ResourceQuery -> ResourceStatusSummary", "INVARIANT": "All resources include metadata about their current state", "LAYER": "Service", - "PURPOSE": "Shared service for fetching resource data with Git status and task status" + "PURPOSE": "Shared service for fetching resource data with Git status and task status", + "SIDE_EFFECT": "Queries multiple backends for status" }, "relations": [ { @@ -61605,25 +66584,34 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C5", "detail": { - "actual_complexity": 3, + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ResourceServiceModule [C:3] [TYPE Module] [SEMANTICS resource, git, task, status, superset]\n# @BRIEF Shared service for fetching resource data with Git status and task status\n# @LAYER: Service\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [TaskManagerModels]\n# @RELATION DEPENDS_ON -> [GitService]\n# @INVARIANT: All resources include metadata about their current state\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.models import Task\nfrom ..services.git_service import GitService\n\n\n# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE: None\n # @POST: ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE: env is a valid Environment object\n # @POST: Returns list of dashboards with enhanced metadata\n # @PARAM: env (Environment) - The environment to fetch from\n # @PARAM: tasks (List[Task]) - List of tasks to check for status\n # @RETURN: List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION: CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION: CALLS ->[_get_git_status_for_dashboard]\n # @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE: env is valid; page >= 1; page_size > 0.\n # @POST: Returns page items plus total counters without scanning all pages locally.\n # @PARAM: env (Environment) - Source environment.\n # @PARAM: tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM: page (int) - 1-based page number.\n # @PARAM: page_size (int) - Page size.\n # @RETURN: Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION: CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION: CALLS ->[_get_git_status_for_dashboard]\n # @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE: dashboard_id is a valid integer identifier\n # @POST: Returns the newest llm_dashboard_validation task summary or None\n # @PARAM: dashboard_id (int) - The dashboard ID\n # @PARAM: env_id (Optional[str]) - Environment ID to match task params\n # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN: Optional[Dict] - Task summary with task_id and status\n # @RELATION: CALLS ->[_normalize_datetime_for_compare]\n # @RELATION: CALLS ->[_normalize_validation_status]\n # @RELATION: CALLS ->[_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE: raw_status can be enum or string\n # @POST: Returns uppercase status without enum class prefix\n # @PARAM: raw_status (Any) - Raw task status object/value\n # @RETURN: str - Normalized status token\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE: raw_status can be any scalar type\n # @POST: Returns normalized validation status token or None\n # @PARAM: raw_status (Any) - Raw validation status from task result\n # @RETURN: Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE: value may be datetime or any scalar.\n # @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM: value (Any) - Candidate datetime-like value.\n # @RETURN: datetime - UTC-aware comparable datetime.\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n # @RELATION: USED_BY ->[_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE: env is a valid Environment object\n # @POST: Returns list of datasets with enhanced metadata\n # @PARAM: env (Environment) - The environment to fetch from\n # @PARAM: tasks (List[Task]) - List of tasks to check for status\n # @RETURN: List[Dict] - Datasets with mapped_fields and last_task fields\n # @RELATION: CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION: CALLS ->[_get_last_task_for_resource]\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status\n result = []\n for dataset in datasets:\n # dataset is already a dict, no need to call .dict()\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} datasets with status\")\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE: tasks is a list of Task objects\n # @POST: Returns summary with active_count and recent_tasks\n # @PARAM: tasks (List[Task]) - List of tasks to summarize\n # @RETURN: Dict - Activity summary\n # @RELATION: CALLS ->[_extract_resource_name_from_task]\n # @RELATION: CALLS ->[_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE: dashboard_id is a valid integer\n # @POST: Returns git status or None if no repo exists\n # @PARAM: dashboard_id (int) - The dashboard ID\n # @RETURN: Optional[Dict] - Git status with branch and sync_status\n # @RELATION: CALLS ->[get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE: resource_id is a valid string\n # @POST: Returns task summary or None if no tasks found\n # @PARAM: resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN: Optional[Dict] - Task summary with task_id and status\n # @RELATION: CALLS ->[_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE: task is a valid Task object\n # @POST: Returns resource name or task ID\n # @PARAM: task (Task) - The task to extract from\n # @RETURN: str - Resource name or fallback\n # @RELATION: USED_BY ->[get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE: task is a valid Task object\n # @POST: Returns resource type or 'unknown'\n # @PARAM: task (Task) - The task to extract from\n # @RETURN: str - Resource type\n # @RELATION: USED_BY ->[get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n# #endregion ResourceServiceModule\n" + "body": "# #region ResourceServiceModule [C:5] [TYPE Module] [SEMANTICS resource, git, task, status, superset]\n# @BRIEF Shared service for fetching resource data with Git status and task status\n# @LAYER: Service\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [TaskManagerModels]\n# @RELATION DEPENDS_ON -> [GitService]\n# @INVARIANT: All resources include metadata about their current state\n# @SIDE_EFFECT: Queries multiple backends for status\n# @DATA_CONTRACT: ResourceQuery -> ResourceStatusSummary\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.models import Task\nfrom ..services.git_service import GitService\n\n\n# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE: None\n # @POST: ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE: env is a valid Environment object\n # @POST: Returns list of dashboards with enhanced metadata\n # @PARAM: env (Environment) - The environment to fetch from\n # @PARAM: tasks (List[Task]) - List of tasks to check for status\n # @RETURN: List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION: CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION: CALLS ->[_get_git_status_for_dashboard]\n # @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE: env is valid; page >= 1; page_size > 0.\n # @POST: Returns page items plus total counters without scanning all pages locally.\n # @PARAM: env (Environment) - Source environment.\n # @PARAM: tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM: page (int) - 1-based page number.\n # @PARAM: page_size (int) - Page size.\n # @RETURN: Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION: CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION: CALLS ->[_get_git_status_for_dashboard]\n # @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE: dashboard_id is a valid integer identifier\n # @POST: Returns the newest llm_dashboard_validation task summary or None\n # @PARAM: dashboard_id (int) - The dashboard ID\n # @PARAM: env_id (Optional[str]) - Environment ID to match task params\n # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN: Optional[Dict] - Task summary with task_id and status\n # @RELATION: CALLS ->[_normalize_datetime_for_compare]\n # @RELATION: CALLS ->[_normalize_validation_status]\n # @RELATION: CALLS ->[_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE: raw_status can be enum or string\n # @POST: Returns uppercase status without enum class prefix\n # @PARAM: raw_status (Any) - Raw task status object/value\n # @RETURN: str - Normalized status token\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE: raw_status can be any scalar type\n # @POST: Returns normalized validation status token or None\n # @PARAM: raw_status (Any) - Raw validation status from task result\n # @RETURN: Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE: value may be datetime or any scalar.\n # @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM: value (Any) - Candidate datetime-like value.\n # @RETURN: datetime - UTC-aware comparable datetime.\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n # @RELATION: USED_BY ->[_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE: env is a valid Environment object\n # @POST: Returns list of datasets with enhanced metadata\n # @PARAM: env (Environment) - The environment to fetch from\n # @PARAM: tasks (List[Task]) - List of tasks to check for status\n # @RETURN: List[Dict] - Datasets with mapped_fields and last_task fields\n # @RELATION: CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION: CALLS ->[_get_last_task_for_resource]\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status\n result = []\n for dataset in datasets:\n # dataset is already a dict, no need to call .dict()\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} datasets with status\")\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE: tasks is a list of Task objects\n # @POST: Returns summary with active_count and recent_tasks\n # @PARAM: tasks (List[Task]) - List of tasks to summarize\n # @RETURN: Dict - Activity summary\n # @RELATION: CALLS ->[_extract_resource_name_from_task]\n # @RELATION: CALLS ->[_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE: dashboard_id is a valid integer\n # @POST: Returns git status or None if no repo exists\n # @PARAM: dashboard_id (int) - The dashboard ID\n # @RETURN: Optional[Dict] - Git status with branch and sync_status\n # @RELATION: CALLS ->[get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE: resource_id is a valid string\n # @POST: Returns task summary or None if no tasks found\n # @PARAM: resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN: Optional[Dict] - Task summary with task_id and status\n # @RELATION: CALLS ->[_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE: task is a valid Task object\n # @POST: Returns resource name or task ID\n # @PARAM: task (Task) - The task to extract from\n # @RETURN: str - Resource name or fallback\n # @RELATION: USED_BY ->[get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE: task is a valid Task object\n # @POST: Returns resource type or 'unknown'\n # @PARAM: task (Task) - The task to extract from\n # @RETURN: str - Resource type\n # @RELATION: USED_BY ->[get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n# #endregion ResourceServiceModule\n" }, { "contract_id": "ResourceService", "contract_type": "Class", "file_path": "backend/src/services/resource_service.py", - "start_line": 19, - "end_line": 481, + "start_line": 21, + "end_line": 483, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -61649,6 +66637,359 @@ "tier_source": "AutoCalculated", "body": "# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE: None\n # @POST: ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE: env is a valid Environment object\n # @POST: Returns list of dashboards with enhanced metadata\n # @PARAM: env (Environment) - The environment to fetch from\n # @PARAM: tasks (List[Task]) - List of tasks to check for status\n # @RETURN: List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION: CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION: CALLS ->[_get_git_status_for_dashboard]\n # @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE: env is valid; page >= 1; page_size > 0.\n # @POST: Returns page items plus total counters without scanning all pages locally.\n # @PARAM: env (Environment) - Source environment.\n # @PARAM: tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM: page (int) - 1-based page number.\n # @PARAM: page_size (int) - Page size.\n # @RETURN: Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION: CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION: CALLS ->[_get_git_status_for_dashboard]\n # @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE: dashboard_id is a valid integer identifier\n # @POST: Returns the newest llm_dashboard_validation task summary or None\n # @PARAM: dashboard_id (int) - The dashboard ID\n # @PARAM: env_id (Optional[str]) - Environment ID to match task params\n # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN: Optional[Dict] - Task summary with task_id and status\n # @RELATION: CALLS ->[_normalize_datetime_for_compare]\n # @RELATION: CALLS ->[_normalize_validation_status]\n # @RELATION: CALLS ->[_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE: raw_status can be enum or string\n # @POST: Returns uppercase status without enum class prefix\n # @PARAM: raw_status (Any) - Raw task status object/value\n # @RETURN: str - Normalized status token\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE: raw_status can be any scalar type\n # @POST: Returns normalized validation status token or None\n # @PARAM: raw_status (Any) - Raw validation status from task result\n # @RETURN: Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE: value may be datetime or any scalar.\n # @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM: value (Any) - Candidate datetime-like value.\n # @RETURN: datetime - UTC-aware comparable datetime.\n # @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]\n # @RELATION: USED_BY ->[_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE: env is a valid Environment object\n # @POST: Returns list of datasets with enhanced metadata\n # @PARAM: env (Environment) - The environment to fetch from\n # @PARAM: tasks (List[Task]) - List of tasks to check for status\n # @RETURN: List[Dict] - Datasets with mapped_fields and last_task fields\n # @RELATION: CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION: CALLS ->[_get_last_task_for_resource]\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status\n result = []\n for dataset in datasets:\n # dataset is already a dict, no need to call .dict()\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} datasets with status\")\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE: tasks is a list of Task objects\n # @POST: Returns summary with active_count and recent_tasks\n # @PARAM: tasks (List[Task]) - List of tasks to summarize\n # @RETURN: Dict - Activity summary\n # @RELATION: CALLS ->[_extract_resource_name_from_task]\n # @RELATION: CALLS ->[_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE: dashboard_id is a valid integer\n # @POST: Returns git status or None if no repo exists\n # @PARAM: dashboard_id (int) - The dashboard ID\n # @RETURN: Optional[Dict] - Git status with branch and sync_status\n # @RELATION: CALLS ->[get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE: resource_id is a valid string\n # @POST: Returns task summary or None if no tasks found\n # @PARAM: resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM: tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN: Optional[Dict] - Task summary with task_id and status\n # @RELATION: CALLS ->[_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE: task is a valid Task object\n # @POST: Returns resource name or task ID\n # @PARAM: task (Task) - The task to extract from\n # @RETURN: str - Resource name or fallback\n # @RELATION: USED_BY ->[get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE: task is a valid Task object\n # @POST: Returns resource type or 'unknown'\n # @PARAM: task (Task) - The task to extract from\n # @RETURN: str - Resource type\n # @RELATION: USED_BY ->[get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n" }, + { + "contract_id": "security_badge_service", + "contract_type": "Module", + "file_path": "backend/src/services/security_badge_service.py", + "start_line": 1, + "end_line": 134, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "Builds security summary and permission badges for profile UI — role extraction," + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], + "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_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_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", + "contract_type": "Class", + "file_path": "backend/src/services/security_badge_service.py", + "start_line": 27, + "end_line": 133, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "build_security_summary returns ProfileSecuritySummary with deterministic sorted permissions.", + "PRE": "plugin_loader may be None for degraded permission discovery.", + "PURPOSE": "Builds security summary with role names and permission badges for profile UI display." + }, + "relations": [ + { + "source_id": "SecurityBadgeService", + "relation_type": "DEPENDS_ON", + "target_id": "User", + "target_ref": "[User]" + }, + { + "source_id": "SecurityBadgeService", + "relation_type": "CALLS", + "target_id": "discover_declared_permissions", + "target_ref": "[discover_declared_permissions]" + } + ], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "build_security_summary", + "contract_type": "Function", + "file_path": "backend/src/services/security_badge_service.py", + "start_line": 42, + "end_line": 103, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returns deterministic security projection for profile UI.", + "PRE": "current_user is authenticated.", + "PURPOSE": "Build read-only security snapshot with role and permission badges." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "_collect_user_permission_pairs", + "contract_type": "Function", + "file_path": "backend/src/services/security_badge_service.py", + "start_line": 105, + "end_line": 122, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returns unique normalized (resource, ACTION) tuples.", + "PRE": "current_user can include role/permission graph.", + "PURPOSE": "Collect effective permission tuples from current user's roles." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "_format_permission_key", + "contract_type": "Function", + "file_path": "backend/src/services/security_badge_service.py", + "start_line": 124, + "end_line": 132, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Convert normalized permission pair to compact UI key." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, + { + "contract_id": "superset_lookup_service", + "contract_type": "Module", + "file_path": "backend/src/services/superset_lookup_service.py", + "start_line": 1, + "end_line": 140, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "LAYER": "Domain", + "PURPOSE": "Environment-scoped Superset account lookup with degradation fallback for network failures.", + "RATIONALE": "Extracted from ProfileService to satisfy INV_7. Superset account lookup has distinct" + }, + "relations": [ + { + "source_id": "superset_lookup_service", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetClient", + "target_ref": "[SupersetClient]" + }, + { + "source_id": "superset_lookup_service", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetAccountLookupAdapter", + "target_ref": "[SupersetAccountLookupAdapter]" + }, + { + "source_id": "superset_lookup_service", + "relation_type": "DEPENDS_ON", + "target_id": "profile_utils", + "target_ref": "[profile_utils]" + } + ], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], + "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_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_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", + "contract_type": "Class", + "file_path": "backend/src/services/superset_lookup_service.py", + "start_line": 31, + "end_line": 139, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "Resolves environments and queries Superset users in selected environment," + }, + "relations": [], + "schema_warnings": [ + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Class' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #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" + }, + { + "contract_id": "_resolve_environment", + "contract_type": "Function", + "file_path": "backend/src/services/superset_lookup_service.py", + "start_line": 128, + "end_line": 138, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Returns environment object when found else None.", + "PRE": "environment_id is provided.", + "PURPOSE": "Resolve environment model from configured environments by id." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " # #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" + }, { "contract_id": "TestSessionConfig", "contract_type": "Module", @@ -67438,7 +72779,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", "start_line": 1, - "end_line": 140, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -67529,7 +72870,7 @@ ], "anchor_syntax": "def", "tier_source": "AutoCalculated", - "body": "# [DEF:TestReportAuditImmutability:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only\n# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.\n# @LAYER: Tests\n# @INVARIANT: Built reports are immutable snapshots; audit hooks produce append-only event traces.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom src.models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n)\nfrom src.services.clean_release.audit_service import (\n audit_check_run,\n audit_preparation,\n audit_report,\n)\nfrom src.services.clean_release.enums import ComplianceDecision, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_terminal_run:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Build deterministic terminal run fixture for report snapshot tests.\n# @PRE: final_status is a valid ComplianceDecision value.\n# @POST: Returns a terminal ComplianceRun suitable for report generation.\ndef _terminal_run(\n final_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> ComplianceRun:\n return ComplianceRun(\n id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n manifest_id=\"manifest-immut-1\",\n manifest_digest=\"digest-immut-1\",\n policy_snapshot_id=\"policy-immut-1\",\n registry_snapshot_id=\"registry-immut-1\",\n requested_by=\"tester\",\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n finished_at=datetime.now(UTC),\n status=RunStatus.SUCCEEDED,\n final_status=final_status,\n )\n\n\n# [/DEF:_terminal_run:Function]\n\n\n# [DEF:test_report_builder_sets_immutable_snapshot_flag:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Ensure generated report payload is marked immutable and persisted as snapshot.\n# @PRE: Terminal run exists.\n# @POST: Built report has immutable=True and repository stores same immutable object.\ndef test_report_builder_sets_immutable_snapshot_flag():\n repository = CleanReleaseRepository()\n builder = ComplianceReportBuilder(repository)\n run = _terminal_run()\n\n report = builder.build_report_payload(run, [])\n persisted = builder.persist_report(report)\n\n assert report.immutable is True\n assert persisted.immutable is True\n assert repository.get_report(report.id) is persisted\n\n\n# [/DEF:test_report_builder_sets_immutable_snapshot_flag:Function]\n\n\n# [DEF:test_repository_rejects_report_overwrite_for_same_report_id:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Define immutability contract that report snapshots cannot be overwritten by same identifier.\n# @PRE: Existing report with id is already persisted.\n# @POST: Second save for same report id is rejected with explicit immutability error.\ndef test_repository_rejects_report_overwrite_for_same_report_id():\n repository = CleanReleaseRepository()\n original = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n final_status=ComplianceDecision.PASSED,\n summary_json={\n \"operator_summary\": \"original\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n mutated = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-2\",\n candidate_id=\"cand-immut-2\",\n final_status=ComplianceDecision.ERROR,\n summary_json={\n \"operator_summary\": \"mutated\",\n \"violations_count\": 1,\n \"blocking_violations_count\": 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n\n repository.save_report(original)\n\n with pytest.raises(ValueError, match=\"immutable\"):\n repository.save_report(mutated)\n\n\n# [/DEF:test_repository_rejects_report_overwrite_for_same_report_id:Function]\n\n\n# [DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.\n# @PRE: Logger backend is patched.\n# @POST: Three calls produce three ordered info entries with molecular prefixes.\n@patch(\"src.services.clean_release.audit_service.logger\")\ndef test_audit_hooks_emit_append_only_event_stream(mock_logger):\n audit_preparation(\"cand-immut-1\", \"PREPARED\")\n audit_check_run(\"run-immut-1\", \"PASSED\")\n audit_report(\"CCR-immut-1\", \"cand-immut-1\")\n\n assert mock_logger.info.call_count == 3\n logged_messages = [call.args[0] for call in mock_logger.info.call_args_list]\n assert logged_messages[0].startswith(\"[REASON]\")\n assert logged_messages[1].startswith(\"[REFLECT]\")\n assert logged_messages[2].startswith(\"[EXPLORE]\")\n\n\n# [/DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n\n# [/DEF:TestReportAuditImmutability:Module]\n" + "body": "# [DEF:TestReportAuditImmutability:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only\n# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.\n# @LAYER: Tests\n# @INVARIANT: Built reports are immutable snapshots; audit hooks produce append-only event traces.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom src.models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n)\nfrom src.services.clean_release.audit_service import (\n audit_check_run,\n audit_preparation,\n audit_report,\n)\nfrom src.services.clean_release.enums import ComplianceDecision, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_terminal_run:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Build deterministic terminal run fixture for report snapshot tests.\n# @PRE: final_status is a valid ComplianceDecision value.\n# @POST: Returns a terminal ComplianceRun suitable for report generation.\ndef _terminal_run(\n final_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> ComplianceRun:\n return ComplianceRun(\n id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n manifest_id=\"manifest-immut-1\",\n manifest_digest=\"digest-immut-1\",\n policy_snapshot_id=\"policy-immut-1\",\n registry_snapshot_id=\"registry-immut-1\",\n requested_by=\"tester\",\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n finished_at=datetime.now(UTC),\n status=RunStatus.SUCCEEDED,\n final_status=final_status,\n )\n\n\n# [/DEF:_terminal_run:Function]\n\n\n# [DEF:test_report_builder_sets_immutable_snapshot_flag:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Ensure generated report payload is marked immutable and persisted as snapshot.\n# @PRE: Terminal run exists.\n# @POST: Built report has immutable=True and repository stores same immutable object.\ndef test_report_builder_sets_immutable_snapshot_flag():\n repository = CleanReleaseRepository()\n builder = ComplianceReportBuilder(repository)\n run = _terminal_run()\n\n report = builder.build_report_payload(run, [])\n persisted = builder.persist_report(report)\n\n assert report.immutable is True\n assert persisted.immutable is True\n assert repository.get_report(report.id) is persisted\n\n\n# [/DEF:test_report_builder_sets_immutable_snapshot_flag:Function]\n\n\n# [DEF:test_repository_rejects_report_overwrite_for_same_report_id:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Define immutability contract that report snapshots cannot be overwritten by same identifier.\n# @PRE: Existing report with id is already persisted.\n# @POST: Second save for same report id is rejected with explicit immutability error.\ndef test_repository_rejects_report_overwrite_for_same_report_id():\n repository = CleanReleaseRepository()\n original = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n final_status=ComplianceDecision.PASSED,\n summary_json={\n \"operator_summary\": \"original\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n mutated = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-2\",\n candidate_id=\"cand-immut-2\",\n final_status=ComplianceDecision.ERROR,\n summary_json={\n \"operator_summary\": \"mutated\",\n \"violations_count\": 1,\n \"blocking_violations_count\": 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n\n repository.save_report(original)\n\n with pytest.raises(ValueError, match=\"immutable\"):\n repository.save_report(mutated)\n\n\n# [/DEF:test_repository_rejects_report_overwrite_for_same_report_id:Function]\n\n\n# [DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.\n# @PRE: Logger backend is patched.\n# @POST: Three calls produce three ordered info entries with molecular prefixes.\n@patch(\"src.services.clean_release.audit_service.logger\")\ndef test_audit_hooks_emit_append_only_event_stream(mock_logger):\n audit_preparation(\"cand-immut-1\", \"PREPARED\")\n audit_check_run(\"run-immut-1\", \"PASSED\")\n audit_report(\"CCR-immut-1\", \"cand-immut-1\")\n\n assert mock_logger.reason.call_count == 1\n assert mock_logger.reflect.call_count == 1\n assert mock_logger.explore.call_count == 1\n\n reason_msg = mock_logger.reason.call_args[0][0]\n reflect_msg = mock_logger.reflect.call_args[0][0]\n explore_msg = mock_logger.explore.call_args[0][0]\n\n assert reason_msg.startswith(\"clean-release preparation\")\n assert reflect_msg.startswith(\"clean-release check_run\")\n assert explore_msg.startswith(\"clean-release report_id\")\n\n\n# [/DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n\n# [/DEF:TestReportAuditImmutability:Module]\n" }, { "contract_id": "_terminal_run", @@ -67728,7 +73069,7 @@ "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", "start_line": 120, - "end_line": 138, + "end_line": 144, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -67785,7 +73126,7 @@ ], "anchor_syntax": "def", "tier_source": "AutoCalculated", - "body": "# [DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.\n# @PRE: Logger backend is patched.\n# @POST: Three calls produce three ordered info entries with molecular prefixes.\n@patch(\"src.services.clean_release.audit_service.logger\")\ndef test_audit_hooks_emit_append_only_event_stream(mock_logger):\n audit_preparation(\"cand-immut-1\", \"PREPARED\")\n audit_check_run(\"run-immut-1\", \"PASSED\")\n audit_report(\"CCR-immut-1\", \"cand-immut-1\")\n\n assert mock_logger.info.call_count == 3\n logged_messages = [call.args[0] for call in mock_logger.info.call_args_list]\n assert logged_messages[0].startswith(\"[REASON]\")\n assert logged_messages[1].startswith(\"[REFLECT]\")\n assert logged_messages[2].startswith(\"[EXPLORE]\")\n\n\n# [/DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n" + "body": "# [DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n# @RELATION: BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.\n# @PRE: Logger backend is patched.\n# @POST: Three calls produce three ordered info entries with molecular prefixes.\n@patch(\"src.services.clean_release.audit_service.logger\")\ndef test_audit_hooks_emit_append_only_event_stream(mock_logger):\n audit_preparation(\"cand-immut-1\", \"PREPARED\")\n audit_check_run(\"run-immut-1\", \"PASSED\")\n audit_report(\"CCR-immut-1\", \"cand-immut-1\")\n\n assert mock_logger.reason.call_count == 1\n assert mock_logger.reflect.call_count == 1\n assert mock_logger.explore.call_count == 1\n\n reason_msg = mock_logger.reason.call_args[0][0]\n reflect_msg = mock_logger.reflect.call_args[0][0]\n explore_msg = mock_logger.explore.call_args[0][0]\n\n assert reason_msg.startswith(\"clean-release preparation\")\n assert reflect_msg.startswith(\"clean-release check_run\")\n assert explore_msg.startswith(\"clean-release report_id\")\n\n\n# [/DEF:test_audit_hooks_emit_append_only_event_stream:Function]\n" }, { "contract_id": "SupersetCompatibilityMatrixTests", @@ -74702,7 +80043,7 @@ ], "anchor_syntax": "def", "tier_source": "AutoCalculated", - "body": "# [DEF:build:Module]\n# @PURPOSE: Unified build script — local docker compose + release bundles + lightweight bundle\n# @COMPLEXITY: 2\n#\n# Usage: ./build.sh [options]\n#\n# Commands:\n# up [profile] Build and start local docker compose (default: current)\n# down [profile] Stop services\n# restart [profile] Rebuild and restart services\n# logs [profile] Tail logs\n# status Show running containers for all profiles\n# bundle Full release bundle (backend + frontend + postgres .tar's)\n# bundle:light Lightweight all-in-one bundle (<200 MB, no Playwright)\n# help Show this help\n#\n# Profiles: current (default), master\n# [/DEF:build:Module]\n" + "body": "# [DEF:build:Module]\n# @PURPOSE: Unified build script — local docker compose + release bundles + lightweight bundle\n# @COMPLEXITY: 2\n#\n# Usage: ./build.sh [options]\n#\n# Commands:\n# up [profile] Build and start local docker compose (default: current)\n# down [profile] Stop services\n# restart [profile] Rebuild and restart services\n# logs [profile] Tail logs\n# status Show running containers for all profiles\n# bundle Full release bundle (backend + frontend + postgres .tar.xz's)\n# bundle:light Lightweight all-in-one bundle (<200 MB, .tar.xz, no Playwright)\n# help Show this help\n#\n# Profiles: current (default), master\n# [/DEF:build:Module]\n" }, { "contract_id": "docker.backend.entrypoint", @@ -74761,10 +80102,19 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TIER", - "message": "@TIER is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TIER value 'STANDARD' is not in allowed enum", + "detail": { + "allowed": [ + "TIER_1", + "TIER_2", + "TIER_3", + "TIER_4", + "TIER_5" + ], + "value": "STANDARD" + } }, { "code": "tag_forbidden_by_complexity", @@ -74838,6 +80188,1140 @@ "tier_source": "AutoCalculated", "body": "# [DEF:docker.backend.entrypoint.bootstrap_admin:Function]\n# @PURPOSE: Execute optional initial admin bootstrap from runtime environment variables.\n# @PRE: Python runtime and backend sources are available inside /app/backend.\n# @POST: Admin is created only when INITIAL_ADMIN_CREATE=true and required credentials are present.\nbootstrap_admin() {\n local create_flag=\"${INITIAL_ADMIN_CREATE:-false}\"\n local username=\"${INITIAL_ADMIN_USERNAME:-}\"\n local password=\"${INITIAL_ADMIN_PASSWORD:-}\"\n local email=\"${INITIAL_ADMIN_EMAIL:-}\"\n\n case \"${create_flag,,}\" in\n true|1|yes|y)\n ;;\n *)\n echo \"[entrypoint] INITIAL_ADMIN_CREATE is disabled; skipping admin bootstrap\"\n return 0\n ;;\n esac\n\n if [[ -z \"${username}\" ]]; then\n echo \"[entrypoint] INITIAL_ADMIN_USERNAME is required when INITIAL_ADMIN_CREATE=true\" >&2\n return 1\n fi\n\n if [[ -z \"${password}\" ]]; then\n echo \"[entrypoint] INITIAL_ADMIN_PASSWORD is required when INITIAL_ADMIN_CREATE=true\" >&2\n return 1\n fi\n\n echo \"[entrypoint] initializing auth database\"\n python3 src/scripts/init_auth_db.py\n\n echo \"[entrypoint] running idempotent admin bootstrap for user '${username}'\"\n if [[ -n \"${email}\" ]]; then\n python3 src/scripts/create_admin.py --username \"${username}\" --password \"${password}\" --email \"${email}\"\n else\n python3 src/scripts/create_admin.py --username \"${username}\" --password \"${password}\"\n fi\n}\n# [/DEF:docker.backend.entrypoint.bootstrap_admin:Function]\n" }, + { + "contract_id": "docker.backend.entrypoint.install_playwright", + "contract_type": "Function", + "file_path": "docker/backend.entrypoint.sh", + "start_line": 54, + "end_line": 72, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "PURPOSE": "Lazily install Playwright Chromium browser on first container start." + }, + "relations": [], + "schema_warnings": [ + { + "code": "def_format_deprecated", + "tag": "FORMAT", + "message": "Contract 'docker.backend.entrypoint.install_playwright' uses legacy [DEF:] format. Consider migrating to #region/#endregion for new code. Use axiom_contract_refactor with operation='migrate_def_to_region' to auto-convert.", + "detail": { + "current_format": "def", + "migration_hint": "Use axiom_contract_refactor operation=migrate_def_to_region", + "recommended_format": "region" + } + } + ], + "anchor_syntax": "def", + "tier_source": "AutoCalculated", + "body": "# [DEF:docker.backend.entrypoint.install_playwright:Function]\n# @PURPOSE: Lazily install Playwright Chromium browser on first container start.\n# Browser binaries (~377 MB) are downloaded only once and cached in\n# PLAYWRIGHT_BROWSERS_PATH (default: ~/.cache/ms-playwright).\n# Use a Docker volume or bind mount to persist across restarts:\n# -v playwright_cache:/root/.cache/ms-playwright\ninstall_playwright() {\n local pw_dir=\"${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}\"\n if [ -f \"${pw_dir}/chromium-*/chrome-linux64/chrome\" ]; then\n echo \"[entrypoint] Playwright Chromium already installed at ${pw_dir}\"\n return 0\n fi\n echo \"[entrypoint] Installing Playwright Chromium browser (first start only)...\"\n echo \"[entrypoint] This downloads ~377 MB. Use a volume to cache:\"\n echo \" -v playwright_cache:/root/.cache/ms-playwright\"\n python3 -m playwright install chromium --with-deps 2>&1\n echo \"[entrypoint] Playwright Chromium installed successfully\"\n}\n# [/DEF:docker.backend.entrypoint.install_playwright:Function]\n" + }, + { + "contract_id": "AuthFixture", + "contract_type": "Module", + "file_path": "frontend/e2e/fixtures/auth.fixture.js", + "start_line": 1, + "end_line": 50, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Playwright fixture for authenticated browser contexts.", + "UX_STATE": "LoginError -> Error banner visible on failed login." + }, + "relations": [ + { + "source_id": "AuthFixture", + "relation_type": "DEPENDS_ON", + "target_id": "ApiHelper", + "target_ref": "[ApiHelper]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Module'", + "detail": { + "actual_type": "Module", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region AuthFixture [C:2] [TYPE Module] [SEMANTICS e2e, auth, fixture, test]\n// @BRIEF Playwright fixture for authenticated browser contexts.\n// @RELATION DEPENDS_ON -> [ApiHelper]\n// @UX_STATE LoggedIn -> User sees dashboard page after login.\n// @UX_STATE LoginError -> Error banner visible on failed login.\n\nimport { test as base } from '@playwright/test';\nimport { apiGet, apiPost } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\nconst USERNAME = process.env.E2E_USERNAME || 'admin';\nconst PASSWORD = process.env.E2E_PASSWORD || 'admin123';\n\n/**\n * Extended test fixture with helpers for authenticated browsing.\n *\n * `authPage` — a page that is already logged in (navigated through login form).\n * `api` — direct API helpers for test setup/teardown.\n */\nexport const test = base.extend({\n\t/**\n\t * Authenticated page — logs in via the UI form before each test.\n\t */\n\tauthPage: async ({ page }, use) => {\n\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\t\tawait page.waitForSelector('text=Вход', { timeout: 10_000 });\n\n\t\t// Fill login form — the actual IDs vary by locale, use label text\n\t\tawait page.getByLabel(/Имя пользователя|Username/).fill(USERNAME);\n\t\tawait page.getByLabel(/Пароль|Password/).fill(PASSWORD);\n\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\n\t\t// Wait for redirect to dashboard\n\t\tawait page.waitForURL(/\\/dashboards|\\/datasets|\\//, { timeout: 15_000 });\n\t\t// Ensure navigation sidebar loaded\n\t\tawait page.waitForSelector('nav', { timeout: 10_000 });\n\n\t\tawait use(page);\n\t},\n\n\t/**\n\t * Expose API helpers directly in tests.\n\t */\n\tapi: async ({}, use) => {\n\t\tawait use({ get: apiGet, post: apiPost });\n\t},\n});\n\nexport { expect } from '@playwright/test';\n// #endregion AuthFixture\n" + }, + { + "contract_id": "GlobalSetup", + "contract_type": "Script", + "file_path": "frontend/e2e/fixtures/global-setup.js", + "start_line": 1, + "end_line": 65, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Exits with 1 if either service is down, else sets ENV vars.", + "PRE": "Backend and frontend must be reachable.", + "PURPOSE": "Pre-flight check before any E2E tests run." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Script'", + "detail": { + "actual_type": "Script", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Script' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Script" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "POST", + "message": "@POST is not allowed for contract type 'Script'", + "detail": { + "actual_type": "Script", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Script' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Script" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PRE", + "message": "@PRE is not allowed for contract type 'Script'", + "detail": { + "actual_type": "Script", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Script' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Script" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Script'", + "detail": { + "actual_type": "Script", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Script' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Script" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region GlobalSetup [C:2] [TYPE Script] [SEMANTICS e2e, setup, healthcheck]\n// @BRIEF Pre-flight check before any E2E tests run.\n// @PRE Backend and frontend must be reachable.\n// @POST Exits with 1 if either service is down, else sets ENV vars.\n\nimport { chromium } from '@playwright/test';\n\nconst BACKEND_URL = process.env.BACKEND_URL || 'http://127.0.0.1:8101';\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\n\nasync function globalSetup() {\n\tconsole.log(`[E2E] Pre-flight check — Backend: ${BACKEND_URL}, Frontend: ${FRONTEND_URL}`);\n\n\t// 1. Check backend health\n\ttry {\n\t\tconst healthRes = await fetch(`${BACKEND_URL}/api/health/summary`);\n\t\tif (!healthRes.ok) {\n\t\t\tthrow new Error(`Backend health check failed: ${healthRes.status}`);\n\t\t}\n\t\tconsole.log('[E2E] Backend is healthy ✅');\n\t} catch (err) {\n\t\tconsole.error(`[E2E] Backend unreachable: ${err.message}`);\n\t\tconsole.error('[E2E] Make sure the stack is running: docker compose up -d');\n\t\tprocess.exit(1);\n\t}\n\n\t// 2. Check frontend responds\n\ttry {\n\t\tconst frontRes = await fetch(FRONTEND_URL);\n\t\tif (!frontRes.ok && frontRes.status !== 302) {\n\t\t\tconsole.warn(`[E2E] Frontend responded with ${frontRes.status}, continuing anyway`);\n\t\t} else {\n\t\t\tconsole.log('[E2E] Frontend is reachable ✅');\n\t\t}\n\t} catch (err) {\n\t\tconsole.error(`[E2E] Frontend unreachable: ${err.message}`);\n\t\tprocess.exit(1);\n\t}\n\n\t// 3. Verify auth works (can get token)\n\ttry {\n\t\tconst loginRes = await fetch(`${BACKEND_URL}/api/auth/login`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/x-www-form-urlencoded' },\n\t\t\tbody: `username=${encodeURIComponent(process.env.E2E_USERNAME || 'admin')}&password=${encodeURIComponent(process.env.E2E_PASSWORD || 'admin123')}`,\n\t\t});\n\t\tif (!loginRes.ok) {\n\t\t\tthrow new Error(`Login failed: ${loginRes.status}`);\n\t\t}\n\t\tconst loginData = await loginRes.json();\n\t\tif (!loginData.access_token) {\n\t\t\tthrow new Error('No access_token in login response');\n\t\t}\n\t\tprocess.env.E2E_TOKEN = loginData.access_token;\n\t\tconsole.log('[E2E] Auth token obtained ✅');\n\t} catch (err) {\n\t\tconsole.error(`[E2E] Auth check failed: ${err.message}`);\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log('[E2E] Pre-flight complete — all systems go 🚀');\n}\n\nexport default globalSetup;\n// #endregion GlobalSetup\n" + }, + { + "contract_id": "ApiHelper", + "contract_type": "Module", + "file_path": "frontend/e2e/helpers/api.helper.js", + "start_line": 1, + "end_line": 89, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Helper for backend API calls in E2E tests (settings CRUD, etc.)" + }, + "relations": [ + { + "source_id": "ApiHelper", + "relation_type": "DEPENDS_ON", + "target_id": "GlobalSetup", + "target_ref": "[GlobalSetup]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region ApiHelper [C:2] [TYPE Module] [SEMANTICS e2e, api, helper, auth]\n// @BRIEF Helper for backend API calls in E2E tests (settings CRUD, etc.)\n// @RELATION DEPENDS_ON -> [GlobalSetup]\n\nconst BACKEND_URL = process.env.BACKEND_URL || 'http://127.0.0.1:8101';\n\n/**\n * Get auth token from env (set during global-setup).\n */\nfunction getToken() {\n\treturn process.env.E2E_TOKEN;\n}\n\n/**\n * Make an authenticated GET request to the backend API.\n */\nexport async function apiGet(path) {\n\tconst token = getToken();\n\tconst res = await fetch(`${BACKEND_URL}${path}`, {\n\t\theaders: {\n\t\t\t'Authorization': `Bearer ${token}`,\n\t\t\t'Content-Type': 'application/json',\n\t\t},\n\t});\n\tif (!res.ok) {\n\t\tconst body = await res.text().catch(() => '');\n\t\tthrow new Error(`GET ${path} → ${res.status}: ${body.slice(0, 200)}`);\n\t}\n\treturn res.json();\n}\n\n/**\n * Make an authenticated POST request to the backend API.\n */\nexport async function apiPost(path, body = {}) {\n\tconst token = getToken();\n\tconst res = await fetch(`${BACKEND_URL}${path}`, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Authorization': `Bearer ${token}`,\n\t\t\t'Content-Type': 'application/json',\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => '');\n\t\tthrow new Error(`POST ${path} → ${res.status}: ${text.slice(0, 200)}`);\n\t}\n\treturn res.status === 204 ? null : res.json();\n}\n\n/**\n * Make an authenticated PUT request to the backend API.\n */\nexport async function apiPut(path, body = {}) {\n\tconst token = getToken();\n\tconst res = await fetch(`${BACKEND_URL}${path}`, {\n\t\tmethod: 'PUT',\n\t\theaders: {\n\t\t\t'Authorization': `Bearer ${token}`,\n\t\t\t'Content-Type': 'application/json',\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t});\n\tif (!res.ok) {\n\t\tconst text = await res.text().catch(() => '');\n\t\tthrow new Error(`PUT ${path} → ${res.status}: ${text.slice(0, 200)}`);\n\t}\n\treturn res.json();\n}\n\n/**\n * Make an authenticated DELETE request to the backend API.\n */\nexport async function apiDelete(path) {\n\tconst token = getToken();\n\tconst res = await fetch(`${BACKEND_URL}${path}`, {\n\t\tmethod: 'DELETE',\n\t\theaders: {\n\t\t\t'Authorization': `Bearer ${token}`,\n\t\t},\n\t});\n\tif (!res.ok && res.status !== 204) {\n\t\tconst text = await res.text().catch(() => '');\n\t\tthrow new Error(`DELETE ${path} → ${res.status}: ${text.slice(0, 200)}`);\n\t}\n\treturn res.status === 204 ? null : res.json();\n}\n// #endregion ApiHelper\n" + }, + { + "contract_id": "frontend/e2e/helpers/api.helper.js::getToken", + "contract_type": "Function", + "file_path": "frontend/e2e/helpers/api.helper.js", + "start_line": 7, + "end_line": 10, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Get auth token from env (set during global-setup).\n */" + }, + { + "contract_id": "frontend/e2e/helpers/api.helper.js::apiGet", + "contract_type": "Function", + "file_path": "frontend/e2e/helpers/api.helper.js", + "start_line": 14, + "end_line": 17, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Make an authenticated GET request to the backend API.\n */" + }, + { + "contract_id": "frontend/e2e/helpers/api.helper.js::apiPost", + "contract_type": "Function", + "file_path": "frontend/e2e/helpers/api.helper.js", + "start_line": 32, + "end_line": 35, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Make an authenticated POST request to the backend API.\n */" + }, + { + "contract_id": "frontend/e2e/helpers/api.helper.js::apiPut", + "contract_type": "Function", + "file_path": "frontend/e2e/helpers/api.helper.js", + "start_line": 52, + "end_line": 55, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Make an authenticated PUT request to the backend API.\n */" + }, + { + "contract_id": "frontend/e2e/helpers/api.helper.js::apiDelete", + "contract_type": "Function", + "file_path": "frontend/e2e/helpers/api.helper.js", + "start_line": 72, + "end_line": 75, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Make an authenticated DELETE request to the backend API.\n */" + }, + { + "contract_id": "GitE2E", + "contract_type": "Test", + "file_path": "frontend/e2e/tests/git.e2e.js", + "start_line": 1, + "end_line": 90, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "E2E tests for Git integration — config CRUD, connection test.", + "UX_STATE": "ConnectionTested -> Success/failure toast feedback." + }, + "relations": [ + { + "source_id": "GitE2E", + "relation_type": "VERIFIES", + "target_id": "GitDashboardPage, GitConfigRoutes", + "target_ref": "[GitDashboardPage, GitConfigRoutes]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + } + ], + "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 VERIFIES -> [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" + }, + { + "contract_id": "LoginE2E", + "contract_type": "Test", + "file_path": "frontend/e2e/tests/login.e2e.js", + "start_line": 1, + "end_line": 52, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "E2E tests for login/logout flow.", + "UX_STATE": "InvalidCredentials -> Error toast or message shown." + }, + "relations": [ + { + "source_id": "LoginE2E", + "relation_type": "VERIFIES", + "target_id": "LoginPage", + "target_ref": "[LoginPage]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region LoginE2E [C:3] [TYPE Test] [SEMANTICS e2e, login, auth, ui]\n// @BRIEF E2E tests for login/logout flow.\n// @RELATION VERIFIES -> [LoginPage]\n// @UX_STATE LoginForm -> Form rendered with username, password fields, submit button.\n// @UX_STATE Authenticated -> Redirect to dashboards with sidebar visible.\n// @UX_STATE InvalidCredentials -> Error toast or message shown.\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\n\ntest.describe('Login Flow', () => {\n\ttest('should display login form with required fields', async ({ page }) => {\n\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\n\t\t// Form elements visible\n\t\tawait expect(page.getByRole('heading', { name: /Вход|Login/ })).toBeVisible();\n\t\tawait expect(page.getByLabel(/Имя пользователя|Username/)).toBeVisible();\n\t\tawait expect(page.getByLabel(/Пароль|Password/)).toBeVisible();\n\t\tawait expect(page.getByRole('button', { name: /Вход|Login/ })).toBeVisible();\n\t});\n\n\ttest('should login with valid credentials and redirect', async ({ page }) => {\n\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\n\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\tawait page.getByLabel(/Пароль|Password/).fill('admin123');\n\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\n\t\t// Should redirect away from /login\n\t\tawait page.waitForURL(/\\/(?!login)/, { timeout: 15_000 });\n\t\t// Sidebar navigation should be visible\n\t\tawait expect(page.locator('nav')).toBeVisible();\n\t\t// User menu should exist\n\t\tawait expect(page.getByRole('button', { name: /Меню пользователя|User/ })).toBeVisible();\n\t});\n\n\ttest('should show error on invalid credentials', async ({ page }) => {\n\t\tawait page.goto(`${FRONTEND_URL}/login`);\n\n\t\tawait page.getByLabel(/Имя пользователя|Username/).fill('wrong');\n\t\tawait page.getByLabel(/Пароль|Password/).fill('wrong');\n\t\tawait page.getByRole('button', { name: /Вход|Login/ }).click();\n\n\t\t// Should stay on login page with error feedback\n\t\tawait expect(page.getByRole('heading', { name: /Вход|Login/ })).toBeVisible({ timeout: 10_000 });\n\t\t// Check for error toast or message\n\t\tconst errorVisible = await page.getByText(/ошиб|error|invalid|401|Unauthorized/i).first().isVisible().catch(() => false);\n\t\texpect(errorVisible).toBeTruthy();\n\t});\n});\n// #endregion LoginE2E\n" + }, + { + "contract_id": "MigrationE2E", + "contract_type": "Test", + "file_path": "frontend/e2e/tests/migration.e2e.js", + "start_line": 1, + "end_line": 82, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "E2E tests for dataset/environment migration and sync.", + "UX_STATE": "MappingsLoaded -> Synchronized resources table populated." + }, + "relations": [ + { + "source_id": "MigrationE2E", + "relation_type": "VERIFIES", + "target_id": "MigrationApi, SettingsPage", + "target_ref": "[MigrationApi, SettingsPage]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region MigrationE2E [C:3] [TYPE Test] [SEMANTICS e2e, migration, sync, datasets]\n// @BRIEF E2E tests for dataset/environment migration and sync.\n// @RELATION VERIFIES -> [MigrationApi, SettingsPage]\n// @UX_STATE SyncTriggered -> Sync-now request accepted.\n// @UX_STATE MappingsLoaded -> Synchronized resources table populated.\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';\n\ntest.describe('Dataset Environment Sync', () => {\n\ttest('should show migration settings tab', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/migration`);\n\t\tawait authPage.waitForSelector('text=Синхронизация', { timeout: 10_000 });\n\t\t// Schedule section visible\n\t\tawait expect(authPage.getByText(/Cron-выражение|Schedule/)).toBeVisible();\n\t});\n\n\ttest('should trigger sync and check results via API', async ({ authPage }) => {\n\t\t// Get pre-sync settings\n\t\tconst settingsBefore = await apiGet('/api/migration/settings');\n\t\texpect(settingsBefore).toBeDefined();\n\t\texpect(settingsBefore.cron).toBeDefined();\n\n\t\t// Trigger sync (fire-and-forget, it runs async)\n\t\tlet syncResult;\n\t\ttry {\n\t\t\tsyncResult = await apiPost('/api/migration/sync-now', {});\n\t\t\tconsole.log('[E2E] Sync triggered:', JSON.stringify(syncResult).slice(0, 200));\n\t\t} catch (err) {\n\t\t\t// Sync may timeout or fail if Superset is unreachable — that's acceptable for CI\n\t\t\tconsole.log('[E2E] Sync note (non-blocking):', err.message.slice(0, 200));\n\t\t}\n\n\t\t// Verify sync mappings endpoint responds\n\t\tconst mappings = await apiGet('/api/migration/mappings-data?skip=0&limit=5').catch(() => []);\n\t\tconsole.log(`[E2E] Mappings count: ${Array.isArray(mappings) ? mappings.length : 'N/A'}`);\n\n\t\t// If mappings exist, verify structure\n\t\tif (Array.isArray(mappings) && mappings.length > 0) {\n\t\t\tconst mapping = mappings[0];\n\t\t\texpect(mapping.resource_name).toBeDefined();\n\t\t\texpect(mapping.resource_type).toBeDefined();\n\t\t}\n\t});\n});\n\ntest.describe('Datasets', () => {\n\ttest('should list datasets from ss-dev', async ({ authPage }) => {\n\t\tconst datasets = await apiGet('/api/datasets?env_id=ss-dev&skip=0&limit=10');\n\t\texpect(Array.isArray(datasets)).toBeTruthy();\n\t\tif (datasets.length > 0) {\n\t\t\texpect(datasets[0].id || datasets[0].table_name).toBeTruthy();\n\t\t}\n\t});\n\n\ttest('should show datasets page in UI', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/datasets`);\n\t\tawait authPage.waitForSelector('text=Датасеты', { timeout: 10_000 });\n\t\t// Dataset table/list skeleton\n\t\tawait expect(authPage.locator('main')).toBeVisible();\n\t});\n\n\ttest('should show translate_fact dataset detail', async ({ authPage }) => {\n\t\t// Get translate_fact datasource ID\n\t\tconst datasources = await apiGet('/api/translate/datasources?env_id=ss-dev');\n\t\tconst translateFact = datasources.find(ds =>\n\t\t\tds.table_name === 'translate_fact' && ds.schema === 'dm_view'\n\t\t);\n\t\texpect(translateFact).toBeDefined();\n\t\texpect(translateFact.id).toBeDefined();\n\n\t\t// Navigate to dataset detail in UI\n\t\tawait authPage.goto(`${FRONTEND_URL}/datasets/${translateFact.id}?env_id=ss-dev`);\n\t\tawait authPage.waitForTimeout(3000);\n\t\t// Page should load\n\t\tconst bodyText = await authPage.locator('body').innerText();\n\t\texpect(bodyText.length).toBeGreaterThan(0);\n\t});\n});\n// #endregion MigrationE2E\n" + }, + { + "contract_id": "SettingsE2E", + "contract_type": "Test", + "file_path": "frontend/e2e/tests/settings.e2e.js", + "start_line": 1, + "end_line": 78, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "E2E tests for Settings page — environments, LLM providers, Git config.", + "UX_STATE": "EnvironmentAdded -> New Superset env appears in list." + }, + "relations": [ + { + "source_id": "SettingsE2E", + "relation_type": "VERIFIES", + "target_id": "SettingsPage", + "target_ref": "[SettingsPage]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region SettingsE2E [C:3] [TYPE Test] [SEMANTICS e2e, settings, environments, llm, git]\n// @BRIEF E2E tests for Settings page — environments, LLM providers, Git config.\n// @RELATION VERIFIES -> [SettingsPage]\n// @UX_STATE TabNavigated -> Active tab content is displayed.\n// @UX_STATE EnvironmentAdded -> New Superset env appears in list.\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiPost, apiDelete } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\n\ntest.describe('Settings — Environments', () => {\n\ttest.beforeEach(async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/environments`);\n\t\tawait authPage.waitForSelector('text=Окружения', { timeout: 10_000 });\n\t});\n\n\ttest('should display environments tab with list', async ({ authPage }) => {\n\t\t// Tab button is active\n\t\tawait expect(authPage.getByRole('button', { name: 'Окружения' })).toBeVisible();\n\t\t// At least one environment column header visible\n\t\tawait expect(authPage.getByText(/НАЗВАНИЕ ПОДКЛЮЧЕНИЯ|URL|ИМЯ ПОЛЬЗОВАТЕЛЯ/)).toBeVisible();\n\t});\n\n\ttest('should show add environment form', async ({ authPage }) => {\n\t\tawait authPage.getByRole('button', { name: /Добавить окружение|Add/ }).click();\n\t\tawait expect(authPage.getByText('ID')).toBeVisible();\n\t\tawait expect(authPage.getByRole('button', { name: /Отмена|Cancel/ })).toBeVisible();\n\t\tawait expect(authPage.getByRole('button', { name: /Добавить окружение|Save/ })).toBeVisible();\n\t});\n\n\ttest('should add and delete environment via UI', async ({ authPage }) => {\n\t\tconst envName = `e2e-test-${Date.now()}`;\n\n\t\t// Open form\n\t\tawait authPage.getByRole('button', { name: /Добавить окружение|Add/ }).click();\n\n\t\t// Fill form (fields order depends on form layout)\n\t\tawait authPage.getByLabel('ID').fill(envName);\n\t\tawait authPage.getByLabel(/Название подключения|Name/).fill(envName);\n\t\tawait authPage.getByLabel('URL').fill('https://e2e-test.example.com');\n\t\tawait authPage.getByLabel(/Имя пользователя|Username/).fill('admin');\n\t\tawait authPage.getByLabel(/Пароль|Password/).fill('test-pass');\n\t\tawait authPage.getByRole('button', { name: /Добавить окружение|Save/ }).click();\n\n\t\t// Should appear in list\n\t\tawait expect(authPage.getByText(envName)).toBeVisible({ timeout: 10_000 });\n\t});\n\n\ttest('should navigate between all settings tabs', async ({ authPage }) => {\n\t\tconst tabs = ['Окружения', 'Настройка логирования', 'Подключения', 'Интеграция Git', 'LLM',\n\t\t\t'Синхронизация миграции', 'Хранилище', 'Разделы проекта', 'Automation'];\n\n\t\tfor (const tab of tabs) {\n\t\t\tawait authPage.getByRole('button', { name: tab }).click();\n\t\t\tawait expect(authPage.getByRole('button', { name: tab })).toHaveClass(/text-blue-600|border-blue-600/);\n\t\t}\n\t});\n});\n\ntest.describe('Settings — LLM Provider', () => {\n\ttest('should show LLM tab with provider list', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/llm`);\n\t\tawait authPage.waitForSelector('text=LLM', { timeout: 10_000 });\n\t\t// LLM provider config section visible\n\t\tawait expect(authPage.getByText(/LLM/).first()).toBeVisible();\n\t});\n});\n\ntest.describe('Settings — Git Integration', () => {\n\ttest('should show Git tab with config list', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\tawait authPage.waitForSelector('text=Интеграция Git', { timeout: 10_000 });\n\t\t// Configured servers section\n\t\tawait expect(authPage.getByText(/Настроенные серверы/)).toBeVisible();\n\t});\n});\n// #endregion SettingsE2E\n" + }, + { + "contract_id": "SmokeE2E", + "contract_type": "Test", + "file_path": "frontend/e2e/tests/smoke.e2e.js", + "start_line": 1, + "end_line": 136, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "PURPOSE": "Golden-path smoke test: login → settings → translate → git → verify.", + "RATIONALE": "Single sequential test verifies the entire stack without per-test setup overhead.", + "UX_STATE": "FullCycle -> All key user journeys executed sequentially." + }, + "relations": [ + { + "source_id": "SmokeE2E", + "relation_type": "VERIFIES", + "target_id": "LoginPage, SettingsPage, TranslateJob, GitConfig", + "target_ref": "[LoginPage, SettingsPage, TranslateJob, GitConfig]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Test' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Test' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "RATIONALE", + "message": "@RATIONALE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Test' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Test' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Test" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Test' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Test" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region SmokeE2E [C:4] [TYPE Test] [SEMANTICS e2e, smoke, golden-path, full-stack]\n// @BRIEF Golden-path smoke test: login → settings → translate → git → verify.\n// @RELATION VERIFIES -> [LoginPage, SettingsPage, TranslateJob, GitConfig]\n// @UX_STATE FullCycle -> All key user journeys executed sequentially.\n// @RATIONALE Single sequential test verifies the entire stack without per-test setup overhead.\n// Runs in ~30s when backend is warm. Mirrors the manual E2E check.\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiGet, apiPost, apiPut } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\n\ntest.describe('Golden Path — Full Stack E2E', () => {\n\n\ttest('complete user journey: login → settings → translate → git', async ({ page }) => {\n\t\t// ── 1. LOGIN ───────────────────────────────────────────\n\t\tawait test.step('Login as admin', async () => {\n\t\t\tawait page.goto(`${FRONTEND_URL}/login`);\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 expect(page.locator('nav')).toBeVisible();\n\t\t\tconsole.log('[E2E] ✅ Login successful');\n\t\t});\n\n\t\t// ── 2. DASHBOARDS LANDING ──────────────────────────────\n\t\tawait test.step('Dashboard page loads', async () => {\n\t\t\tconst currentUrl = page.url();\n\t\t\texpect(currentUrl).toMatch(/dashboards|\\/datasets|\\//);\n\t\t\tconsole.log(`[E2E] ✅ Redirected to: ${currentUrl}`);\n\t\t});\n\n\t\t// ── 3. SETTINGS — check environments via UI ────────────\n\t\tawait test.step('Settings — environments tab', async () => {\n\t\t\tawait page.goto(`${FRONTEND_URL}/settings#/settings/environments`);\n\t\t\tawait page.waitForSelector('text=Окружения', { timeout: 10_000 });\n\n\t\t\t// Verify environments exist via API\n\t\t\tconst envs = await apiGet('/api/settings/environments');\n\t\t\texpect(envs.length).toBeGreaterThanOrEqual(3);\n\t\t\tconst envNames = envs.map(e => e.id);\n\t\t\texpect(envNames).toContain('ss-dev');\n\t\t\texpect(envNames).toContain('ss-preprod');\n\t\t\texpect(envNames).toContain('ss-prod');\n\t\t\tconsole.log('[E2E] ✅ Environments configured:', envNames.join(', '));\n\t\t});\n\n\t\t// ── 4. SETTINGS — check LLM via API ────────────────────\n\t\tawait test.step('Settings — LLM provider', async () => {\n\t\t\tconst status = await apiGet('/api/llm/status');\n\t\t\texpect(status.configured).toBe(true);\n\t\t\texpect(status.provider_count).toBeGreaterThanOrEqual(1);\n\t\t\tconsole.log(`[E2E] ✅ LLM configured: ${status.provider_name} (${status.default_model})`);\n\t\t});\n\n\t\t// ── 5. SETTINGS — check Git via UI ─────────────────────\n\t\tawait test.step('Settings — Git integration', async () => {\n\t\t\tawait page.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\t\tawait page.waitForSelector('text=Интеграция Git', { timeout: 10_000 });\n\n\t\t\tconst configs = await apiGet('/api/git/config');\n\t\t\texpect(configs.length).toBeGreaterThanOrEqual(1);\n\t\t\t// At least one config visible in UI\n\t\t\tawait expect(page.getByText(configs[0].name).first()).toBeVisible({ timeout: 5_000 });\n\t\t\tconsole.log(`[E2E] ✅ Git configured: ${configs[0].name} (${configs[0].provider})`);\n\t\t});\n\n\t\t// ── 6. DATASETS — verify datasources ───────────────────\n\t\tawait test.step('Datasets — verify datasources', async () => {\n\t\t\tconst datasources = await apiGet('/api/translate/datasources?env_id=ss-dev');\n\t\t\tconst translateFact = datasources.find(ds =>\n\t\t\t\tds.table_name === 'translate_fact' && ds.schema === 'dm_view'\n\t\t\t);\n\t\t\texpect(translateFact).toBeDefined();\n\t\t\texpect(translateFact.id).toBe('33');\n\t\t\tconsole.log(`[E2E] ✅ Dataset dm_view.translate_found (id=${translateFact.id})`);\n\t\t});\n\n\t\t// ── 7. TRANSLATION — create job via API ────────────────\n\t\tawait test.step('Translation — create job', async () => {\n\t\t\tconst providers = await apiGet('/api/llm/providers');\n\t\t\tconst job = await apiPost('/api/translate/jobs', {\n\t\t\t\tname: `E2E Smoke Test ${Date.now()}`,\n\t\t\t\tdescription: 'Golden path E2E test',\n\t\t\t\tsource_dialect: 'postgresql',\n\t\t\t\ttarget_dialect: 'postgresql',\n\t\t\t\tsource_datasource_id: '33',\n\t\t\t\tsource_table: 'dm_view.translate_fact',\n\t\t\t\ttarget_schema: 'dm_view',\n\t\t\t\ttarget_table: 'financial_comments_translated',\n\t\t\t\ttranslation_column: 'comment',\n\t\t\t\ttarget_column: 'comment_translated',\n\t\t\t\ttarget_languages: ['ru'],\n\t\t\t\tbatch_size: 10,\n\t\t\t\tenvironment_id: 'ss-dev',\n\t\t\t\tprovider_id: providers[0]?.id || null,\n\t\t\t});\n\t\t\texpect(job.id).toBeTruthy();\n\t\t\texpect(job.status).toBe('DRAFT');\n\t\t\tconsole.log(`[E2E] ✅ Translation job created: ${job.id} (${job.status})`);\n\n\t\t\t// Verify in UI\n\t\t\tawait page.goto(`${FRONTEND_URL}/translate`);\n\t\t\tawait page.waitForSelector('text=Задания перевода', { timeout: 10_000 });\n\t\t\tawait expect(page.getByText(job.name).first()).toBeVisible({ timeout: 10_000 });\n\t\t});\n\n\t\t// ── 8. GIT — verify repositories ───────────────────────\n\t\tawait test.step('Git — list repositories', async () => {\n\t\t\tconst configs = await apiGet('/api/git/config');\n\t\t\tif (configs.length > 0 && configs[0].provider === 'GITEA') {\n\t\t\t\tconst repos = await apiGet(`/api/git/config/${configs[0].id}/gitea/repos`);\n\t\t\t\texpect(Array.isArray(repos)).toBe(true);\n\t\t\t\tconsole.log(`[E2E] ✅ Gitea repos: ${repos.length} found`);\n\n\t\t\t\t// Verify in git UI\n\t\t\t\tawait page.goto(`${FRONTEND_URL}/settings#/settings/git`);\n\t\t\t\tawait page.waitForTimeout(2000);\n\t\t\t}\n\t\t});\n\n\t\t// ── 9. RUN TRANSLATION (or verify it was attempted) ────\n\t\tawait test.step('Translation — run job (verify pipeline)', async () => {\n\t\t\tconst jobs = await apiGet('/api/translate/jobs');\n\t\t\texpect(Array.isArray(jobs)).toBe(true);\n\t\t\tif (jobs.length > 0) {\n\t\t\t\tconst latestJob = jobs[jobs.length - 1];\n\t\t\t\tconsole.log(`[E2E] ✅ Latest job: ${latestJob.name} — ${latestJob.status}`);\n\t\t\t}\n\t\t});\n\n\t\tconsole.log('[E2E] 🎉 Golden path E2E completed successfully');\n\t});\n});\n// #endregion SmokeE2E\n" + }, + { + "contract_id": "TranslationE2E", + "contract_type": "Test", + "file_path": "frontend/e2e/tests/translation.e2e.js", + "start_line": 1, + "end_line": 142, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "E2E tests for the translation job lifecycle: create → preview → accept → run.", + "UX_STATE": "JobRunning -> Run object created with PENDING status." + }, + "relations": [ + { + "source_id": "TranslationE2E", + "relation_type": "VERIFIES", + "target_id": "TranslatePage, TranslateJobRoutes", + "target_ref": "[TranslatePage, TranslateJobRoutes]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Test'", + "detail": { + "actual_type": "Test", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Test' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Test" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region TranslationE2E [C:3] [TYPE Test] [SEMANTICS e2e, translate, job, preview, run]\n// @BRIEF E2E tests for the translation job lifecycle: create → preview → accept → run.\n// @RELATION VERIFIES -> [TranslatePage, TranslateJobRoutes]\n// @UX_STATE JobCreated -> Job appears in list with DRAFT status.\n// @UX_STATE PreviewCreated -> Preview session with sample rows visible.\n// @UX_STATE JobRunning -> Run object created with PENDING status.\n\nimport { test, expect } from '../fixtures/auth.fixture.js';\nimport { apiPost, apiGet, apiPut } from '../helpers/api.helper.js';\n\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\n\ntest.describe('Translation Job Lifecycle', () => {\n\t// Unique identifier for test isolation\n\tconst testSuffix = Date.now();\n\tconst jobName = `E2E Test Job ${testSuffix}`;\n\tlet jobId = null;\n\tlet runId = null;\n\n\ttest.afterEach(async () => {\n\t\t// Cleanup: delete the job if it was created\n\t\tif (jobId) {\n\t\t\ttry {\n\t\t\t\tawait apiPost(`/api/translate/jobs/${jobId}/run`, { full_translation: true }).catch(() => {});\n\t\t\t} catch (_) { /* ignore */ }\n\t\t}\n\t});\n\n\ttest('1. should create a translation job via UI', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/translate`);\n\t\tawait authPage.waitForSelector('text=Задания перевода', { timeout: 10_000 });\n\n\t\t// Click \"New Job\" button\n\t\tawait authPage.getByRole('button', { name: /Новое задание|New/ }).click();\n\n\t\t// Fill job creation form — adapt selectors to actual form\n\t\tawait authPage.waitForTimeout(1000);\n\n\t\t// Fallback: create via API for reliability, then verify in UI list\n\t\tconst job = await apiPost('/api/translate/jobs', {\n\t\t\tname: jobName,\n\t\t\tdescription: 'E2E test translation job',\n\t\t\tsource_dialect: 'postgresql',\n\t\t\ttarget_dialect: 'postgresql',\n\t\t\tsource_table: 'dm_view.translate_fact',\n\t\t\ttarget_schema: 'dm_view',\n\t\t\ttarget_table: 'financial_comments_translated',\n\t\t\ttranslation_column: 'comment',\n\t\t\ttarget_column: 'comment_translated',\n\t\t\ttarget_languages: ['ru'],\n\t\t\tsource_key_cols: ['id'],\n\t\t\ttarget_key_cols: ['id'],\n\t\t\tbatch_size: 10,\n\t\t\tenvironment_id: 'ss-dev',\n\t\t});\n\t\texpect(job).toBeDefined();\n\t\texpect(job.id).toBeTruthy();\n\t\texpect(job.status).toBe('DRAFT');\n\t\tjobId = job.id;\n\n\t\t// Verify job appears in the UI list — refresh page\n\t\tawait authPage.reload();\n\t\tawait authPage.waitForSelector('text=Задания перевода', { timeout: 10_000 });\n\t\tawait expect(authPage.getByText(jobName).first()).toBeVisible({ timeout: 10_000 });\n\t});\n\n\ttest('2. should prepare and run a translation job via API', async ({ authPage }) => {\n\t\t// Create job via API\n\t\tconst job = await apiPost('/api/translate/jobs', {\n\t\t\tname: `${jobName}-api`,\n\t\t\tdescription: 'E2E test via API',\n\t\t\tsource_dialect: 'postgresql',\n\t\t\ttarget_dialect: 'postgresql',\n\t\t\tsource_datasource_id: '33', // dm_view.translate_fact on ss-dev\n\t\t\tsource_table: 'dm_view.translate_fact',\n\t\t\ttarget_schema: 'dm_view',\n\t\t\ttarget_table: 'financial_comments_translated',\n\t\t\ttranslation_column: 'comment',\n\t\t\ttarget_column: 'comment_translated',\n\t\t\ttarget_languages: ['ru'],\n\t\t\tbatch_size: 10,\n\t\t\tenvironment_id: 'ss-dev',\n\t\t});\n\t\tjobId = job.id;\n\t\texpect(job.status).toBe('DRAFT');\n\n\t\t// Assign LLM provider and set READY\n\t\tconst providers = await apiGet('/api/llm/providers');\n\t\texpect(providers.length).toBeGreaterThan(0);\n\t\tconst providerId = providers[0].id;\n\n\t\tawait apiPut(`/api/translate/jobs/${job.id}`, {\n\t\t\tprovider_id: providerId,\n\t\t\tstatus: 'READY',\n\t\t});\n\n\t\t// Create preview\n\t\tconst preview = await apiPost(`/api/translate/jobs/${job.id}/preview`, {\n\t\t\tsample_size: 2,\n\t\t});\n\t\texpect(preview).toBeDefined();\n\t\texpect(preview.id).toBeTruthy();\n\t\texpect(preview.status).toBe('ACTIVE');\n\n\t\t// Accept preview\n\t\tconst accepted = await apiPost(`/api/translate/jobs/${job.id}/preview/accept`, {});\n\t\texpect(accepted.status).toBe('APPLIED');\n\n\t\t// Run the job\n\t\tconst run = await apiPost(`/api/translate/jobs/${job.id}/run`, {\n\t\t\tfull_translation: true,\n\t\t});\n\t\texpect(run).toBeDefined();\n\t\texpect(run.id).toBeTruthy();\n\t\texpect(run.status).toBe('PENDING');\n\t\trunId = run.id;\n\n\t\t// Poll for run completion (up to 30s)\n\t\tlet attempts = 0;\n\t\tconst maxAttempts = 15;\n\t\tlet finalStatus = 'PENDING';\n\t\twhile (attempts < maxAttempts) {\n\t\t\tawait new Promise(r => setTimeout(r, 2000));\n\t\t\tconst status = await apiGet(`/api/translate/runs/${run.id}`);\n\t\t\tfinalStatus = status.status;\n\t\t\tif (['COMPLETED', 'FAILED', 'CANCELLED'].includes(finalStatus)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tattempts++;\n\t\t}\n\t\tconsole.log(`[E2E] Run final status: ${finalStatus} (attempts: ${attempts})`);\n\t\texpect(['COMPLETED', 'FAILED']).toContain(finalStatus);\n\t});\n\n\ttest('3. should show translation history page', async ({ authPage }) => {\n\t\tawait authPage.goto(`${FRONTEND_URL}/translate/history`);\n\t\tawait authPage.waitForSelector('text=История', { timeout: 10_000 });\n\t\t// History page loaded\n\t\tawait expect(authPage.locator('main')).toBeVisible();\n\t});\n});\n// #endregion TranslationE2E\n" + }, + { + "contract_id": "PlaywrightConfig", + "contract_type": "Config", + "file_path": "frontend/playwright.config.js", + "start_line": 1, + "end_line": 52, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "INVARIANT": "All E2E tests run against a fully deployed stack (DB + backend + frontend).", + "PURPOSE": "Playwright E2E test configuration for ss-tools frontend.", + "UX_STATE": "ConfigLoaded -> Browser contexts are created with predefined env settings." + }, + "relations": [ + { + "source_id": "PlaywrightConfig", + "relation_type": "DEPENDS_ON", + "target_id": "EnvConfig", + "target_ref": "[EnvConfig]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Config'", + "detail": { + "actual_type": "Config", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Config' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Config" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "INVARIANT", + "message": "@INVARIANT is not allowed for contract type 'Config'", + "detail": { + "actual_type": "Config", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Config' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Config" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Config'", + "detail": { + "actual_type": "Config", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Config' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Config" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Config'", + "detail": { + "actual_type": "Config", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Config' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Config" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Config' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Config" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region PlaywrightConfig [C:3] [TYPE Config] [SEMANTICS e2e, playwright, test, config]\n// @BRIEF Playwright E2E test configuration for ss-tools frontend.\n// @RELATION DEPENDS_ON -> [EnvConfig]\n// @INVARIANT All E2E tests run against a fully deployed stack (DB + backend + frontend).\n// @UX_STATE ConfigLoaded -> Browser contexts are created with predefined env settings.\n\nimport { defineConfig, devices } from '@playwright/test';\n\nconst BACKEND_URL = process.env.BACKEND_URL || 'http://127.0.0.1:8101';\nconst FRONTEND_URL = process.env.FRONTEND_URL || 'http://127.0.0.1:8102';\nconst AUTH_USERNAME = process.env.E2E_USERNAME || 'admin';\nconst AUTH_PASSWORD = process.env.E2E_PASSWORD || 'admin123';\n\nexport default defineConfig({\n\ttestDir: './e2e/tests',\n\ttestMatch: '**/*.e2e.js',\n\tfullyParallel: false,\n\tretries: process.env.CI ? 1 : 0,\n\tworkers: 1, // sequential tests — settings mutate global state\n\treporter: [\n\t\t['list'],\n\t\t['html', { outputFolder: 'playwright-report' }],\n\t],\n\ttimeout: 60_000,\n\texpect: {\n\t\ttimeout: 15_000,\n\t},\n\tuse: {\n\t\tbaseURL: FRONTEND_URL,\n\t\ttrace: 'retain-on-failure',\n\t\tscreenshot: 'only-on-failure',\n\t\tvideo: 'retain-on-failure',\n\t},\n\tprojects: [\n\t\t{\n\t\t\tname: 'chromium',\n\t\t\tuse: {\n\t\t\t\t...devices['Desktop Chrome'],\n\t\t\t\tviewport: { width: 1440, height: 900 },\n\t\t\t},\n\t\t},\n\t],\n\t// Global setup — runs once per worker\n\tglobalSetup: './e2e/fixtures/global-setup.js',\n\t// Expose env vars to tests\n\tglobalTeardown: undefined,\n\t// Share backend URL via env\n\t// Each test file can access process.env.BACKEND_URL\n});\n\nexport { BACKEND_URL, FRONTEND_URL, AUTH_USERNAME, AUTH_PASSWORD };\n// #endregion PlaywrightConfig\n" + }, { "contract_id": "handleValidate:Function", "contract_type": "Function", @@ -75357,7 +81841,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n \n Superset Tools\n \n \n
\n\n" + "body": "\n\n\n\n\n\n
\n \n Superset Tools\n \n \n
\n\n" }, { "contract_id": "PasswordPrompt", @@ -75475,7 +81959,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n{#if show}\n \n \n \n \n\n \n\n \n
\n
\n \n \n \n \n \n
\n \n \n Database Password Required\n \n
\n

\n The migration process requires passwords for\n the following databases to proceed.\n

\n\n {#if errorMessage}\n \n Error: {errorMessage}\n
\n {/if}\n\n { e.preventDefault(); handleSubmit(); }}\n class=\"space-y-4\"\n >\n {#each databases as dbName}\n
\n \n Password for {dbName}\n \n \n
\n {/each}\n \n
\n \n \n \n \n \n {submitting ? \"Resuming...\" : \"Resume Migration\"}\n \n \n Cancel\n \n \n \n \n \n{/if}\n\n" + "body": "\n\n\n\n\n\n{#if show}\n \n \n \n \n\n \n\n \n
\n
\n \n \n \n \n \n
\n \n \n Database Password Required\n \n
\n

\n The migration process requires passwords for\n the following databases to proceed.\n

\n\n {#if errorMessage}\n \n Error: {errorMessage}\n
\n {/if}\n\n { e.preventDefault(); handleSubmit(); }}\n class=\"space-y-4\"\n >\n {#each databases as dbName}\n
\n \n Password for {dbName}\n \n \n
\n {/each}\n \n
\n \n \n \n \n \n {submitting ? \"Resuming...\" : \"Resume Migration\"}\n \n \n Cancel\n \n \n \n \n \n{/if}\n\n" }, { "contract_id": "handleSubmit:Function", @@ -76371,7 +82855,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n {#if loading && tasks.length === 0}\n
{$t.tasks?.loading}
\n {:else if tasks.length === 0}\n
{$t.tasks?.no_tasks}
\n {:else}\n
    \n {#each tasks as task (task.id)}\n
  • \n handleTaskClick(task)}\n >\n
    \n
    \n
    \n {task.plugin_id.toUpperCase()}\n {task.id.substring(0, 8)}\n
    \n
    \n

    \n {task.status}\n

    \n
    \n
    \n
    \n
    \n

    \n {#if task.params?.environment_id || task.params?.source_env_id}\n Env: {task.params.environment_id || task.params.source_env_id}\n {/if}\n

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

    \n {$t.tasks?.started?.replace('{time}', formatTime(task.started_at))}\n

    \n
    \n
    \n
    \n \n
  • \n {/each}\n
\n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n
\n {#if loading && tasks.length === 0}\n
{$t.tasks?.loading}
\n {:else if tasks.length === 0}\n
{$t.tasks?.no_tasks}
\n {:else}\n
    \n {#each tasks as task (task.id)}\n
  • \n handleTaskClick(task)}\n >\n
    \n
    \n
    \n {task.plugin_id.toUpperCase()}\n {task.id.substring(0, 8)}\n
    \n
    \n

    \n {task.status}\n

    \n
    \n
    \n
    \n
    \n

    \n {#if task.params?.environment_id || task.params?.source_env_id}\n Env: {task.params.environment_id || task.params.source_env_id}\n {/if}\n

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

    \n {$t.tasks?.started?.replace('{time}', formatTime(task.started_at))}\n

    \n
    \n
    \n
    \n \n
  • \n {/each}\n
\n {/if}\n
\n\n\n" }, { "contract_id": "handleTaskClick:Function", @@ -76412,156 +82896,136 @@ "body": " // #region handleTaskClick:Function [TYPE Function]\n // @PURPOSE: Forwards the selected task through a callback prop.\n // @PRE: taskId is provided.\n // @POST: Parent callback receives task id and task payload.\n function handleTaskClick(task: any) {\n onselect({ id: task.id, task });\n }\n // #endregion handleTaskClick:Function\n" }, { - "contract_id": "handleRealTimeLogs:Action", - "contract_type": "Function", + "contract_id": "TaskLogViewer", + "contract_type": "Component", "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 64, - "end_line": 81, - "tier": "TIER_1", - "complexity": 1, + "start_line": 1, + "end_line": 175, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "POST": "logs are updated with new real-time log entries", - "PRE": "None", - "PURPOSE": "Sync real-time logs to the current log list" + "COMPLEXITY": 3, + "INVARIANT": "Real-time logs are always appended without duplicates.", + "LAYER": "UI", + "PURPOSE": "Displays task logs inline (in drawer) or as modal. Merges real-time WebSocket logs with polled historical logs.", + "SEMANTICS": [ + "task", + "log", + "viewer", + "inline", + "realtime" + ], + "UX_STATE": "Loading -> Default" }, - "relations": [], + "relations": [ + { + "source_id": "TaskLogViewer", + "relation_type": "USES", + "target_id": "taskService", + "target_ref": "taskService" + }, + { + "source_id": "TaskLogViewer", + "relation_type": "USES", + "target_id": "TaskLogPanel", + "target_ref": "TaskLogPanel" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SEMANTICS", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleRealTimeLogs:Action [TYPE Function]\n // @PURPOSE: Sync real-time logs to the current log list\n // @PRE: None\n // @POST: logs are updated with new real-time log entries\n $effect(() => {\n if (realTimeLogs && realTimeLogs.length > 0) {\n const lastLog = realTimeLogs[realTimeLogs.length - 1];\n const exists = logs.some(\n (l) =>\n l.timestamp === lastLog.timestamp &&\n l.message === lastLog.message,\n );\n if (!exists) {\n logs = [...logs, lastLog];\n }\n }\n });\n // #endregion handleRealTimeLogs:Action\n" - }, - { - "contract_id": "fetchLogs:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 83, - "end_line": 103, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "logs are populated with API response", - "PRE": "taskId is set", - "PURPOSE": "Fetches logs for a given task ID" - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region fetchLogs:Function [TYPE Function]\n // @PURPOSE: Fetches logs for a given task ID\n // @PRE: taskId is set\n // @POST: logs are populated with API response\n async function fetchLogs() {\n if (!taskId) return;\n try {\n console.log(`[TaskLogViewer][API][fetchLogs:STARTED] id=${taskId}`);\n logs = await getTaskLogs(taskId);\n console.log(`[TaskLogViewer][API][fetchLogs:SUCCESS] id=${taskId}`);\n } catch (e) {\n console.error(\n `[TaskLogViewer][API][fetchLogs:FAILED] id=${taskId}`,\n e,\n );\n error = e.message;\n } finally {\n loading = false;\n }\n }\n // #endregion fetchLogs:Function\n" - }, - { - "contract_id": "handleFilterChange:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 105, - "end_line": 113, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Log viewer filters updated", - "PRE": "event contains source and level fields", - "PURPOSE": "Updates filter conditions for the log viewer" - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handleFilterChange:Function [TYPE Function]\n // @PURPOSE: Updates filter conditions for the log viewer\n // @PRE: event contains source and level fields\n // @POST: Log viewer filters updated\n function handleFilterChange(event) {\n console.log(\"[TaskLogViewer][UI][handleFilterChange:START]\");\n const { source, level } = event;\n }\n // #endregion handleFilterChange:Function\n" - }, - { - "contract_id": "handleRefresh:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 115, - "end_line": 123, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Logs refetched", - "PRE": "None", - "PURPOSE": "Refreshes the logs by polling the API" - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handleRefresh:Function [TYPE Function]\n // @PURPOSE: Refreshes the logs by polling the API\n // @PRE: None\n // @POST: Logs refetched\n function handleRefresh() {\n console.log(\"[TaskLogViewer][UI][handleRefresh:START]\");\n fetchLogs();\n }\n // #endregion handleRefresh:Function\n" + "body": "\n\n\n\n\n\n{#if shouldShow}\n {#if inline}\n \n
\n {#if loading && logs.length === 0}\n
\n
\n {$t.tasks?.loading}\n
\n {:else if error}\n
\n
\n {error}\n {$t.common?.retry}\n
\n {:else}\n \n {/if}\n
\n {:else}\n \n
\n
\n \n { show = false; onclose(); }}\n role=\"presentation\"\n >
\n\n \n\n
\n
\n
\n

\n {$t.tasks?.logs_title}\n

\n { show = false; onclose(); }}\n aria-label={$t.common?.close}\n >\n \n \n
\n
\n {#if loading && logs.length === 0}\n
\n
\n

{$t.tasks?.loading}

\n
\n {:else if error}\n
\n
\n \n
\n

{error}

\n \n {$t.common?.retry}\n \n
\n {:else}\n \n {/if}\n
\n
\n
\n
\n \n {/if}\n{/if}\n\n" }, { "contract_id": "connect:Function", @@ -76601,6 +83065,44 @@ "tier_source": "AutoCalculated", "body": " // #region connect:Function [TYPE Function]\n /**\n * @purpose Establishes WebSocket connection with exponential backoff and filter parameters.\n * @pre selectedTask must be set in the store.\n * @post WebSocket instance created and listeners attached.\n */\n function connect() {\n const task = get(selectedTask);\n if (!task || connectionStatus === 'completed') return;\n\n console.log(`[TaskRunner][Entry] Connecting to logs for task: ${task.id} (Attempt ${reconnectAttempts + 1}) filters: source=${selectedSource}, level=${selectedLevel}`);\n connectionStatus = 'connecting';\n \n let wsUrl = getWsUrl(task.id);\n \n // Append filter parameters to WebSocket URL\n const params = new URLSearchParams();\n if (selectedSource !== 'all') params.append('source', selectedSource);\n if (selectedLevel !== 'all') params.append('level', selectedLevel);\n \n const queryString = params.toString();\n if (queryString) {\n wsUrl += (wsUrl.includes('?') ? '&' : '?') + queryString;\n }\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = () => {\n console.log('[TaskRunner][Coherence:OK] WebSocket connection established');\n connectionStatus = 'connected';\n reconnectAttempts = 0;\n startDataTimeout();\n };\n\n ws.onmessage = (event) => {\n const logEntry = JSON.parse(event.data);\n taskLogs.update(logs => [...logs, logEntry]);\n resetDataTimeout();\n \n // Check for completion message (if backend sends one)\n if (logEntry.message && logEntry.message.includes('Task completed successfully')) {\n connectionStatus = 'completed';\n ws.close();\n }\n\n // Check for missing mapping signal\n if (logEntry.message && logEntry.message.includes('Missing mapping for database UUID')) {\n const uuidMatch = logEntry.message.match(/UUID: ([\\w-]+)/);\n if (uuidMatch) {\n missingDbInfo = { name: 'Unknown', uuid: uuidMatch[1] };\n connectionStatus = 'awaiting_mapping';\n fetchTargetDatabases();\n showMappingModal = true;\n }\n }\n \n // Check for password request via log context or message\n if (logEntry.message && logEntry.message.includes('Task paused for user input') && logEntry.context && logEntry.context.input_request) {\n const request = logEntry.context.input_request;\n if (request.type === 'database_password') {\n connectionStatus = 'awaiting_input';\n passwordPromptData = {\n databases: request.databases || [],\n errorMessage: request.error_message || ''\n };\n showPasswordPrompt = true;\n }\n }\n };\n\n if (task && task.status === 'AWAITING_INPUT' && task.input_request && task.input_request.type === 'database_password') {\n connectionStatus = 'awaiting_input';\n passwordPromptData = {\n databases: task.input_request.databases || [],\n errorMessage: task.input_request.error_message || ''\n };\n showPasswordPrompt = true;\n }\n \n ws.onerror = (error) => {\n console.error('[TaskRunner][Coherence:Failed] WebSocket error:', error);\n connectionStatus = 'disconnected';\n };\n\n ws.onclose = (event) => {\n console.log(`[TaskRunner][Exit] WebSocket connection closed (Code: ${event.code})`);\n clearTimeout(dataTimeout);\n waitingForData = false;\n\n if (connectionStatus !== 'completed' && reconnectAttempts < maxReconnectAttempts) {\n const delay = Math.min(initialReconnectDelay * Math.pow(2, reconnectAttempts), maxReconnectDelay);\n console.log(`[TaskRunner][Action] Reconnecting in ${delay}ms...`);\n reconnectTimeout = setTimeout(() => {\n reconnectAttempts++;\n connect();\n }, delay);\n } else if (reconnectAttempts >= maxReconnectAttempts) {\n console.error('[TaskRunner][Coherence:Failed] Max reconnect attempts reached.');\n addToast($t.tasks?.log_stream_failed, 'error');\n }\n };\n }\n // #endregion connect:Function\n" }, + { + "contract_id": "handleFilterChange:Function", + "contract_type": "Function", + "file_path": "frontend/src/components/TaskRunner.svelte", + "start_line": 148, + "end_line": 172, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "WebSocket reconnected with new filter parameters, logs cleared.", + "PRE": "event.detail contains source and level filter values.", + "PURPOSE": "Handles filter changes and reconnects WebSocket with new parameters." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " // #region handleFilterChange:Function [TYPE Function]\n /**\n * @purpose Handles filter changes and reconnects WebSocket with new parameters.\n * @pre event.detail contains source and level filter values.\n * @post WebSocket reconnected with new filter parameters, logs cleared.\n */\n function handleFilterChange(event) {\n const { source, level } = event.detail;\n if (selectedSource === source && selectedLevel === level) return;\n \n selectedSource = source;\n selectedLevel = level;\n \n console.log(`[TaskRunner] Filter changed, reconnecting WebSocket: source=${source}, level=${level}`);\n \n // Clear current logs when filter changes to avoid confusion\n taskLogs.set([]);\n \n if (ws) {\n ws.close(); // This will trigger reconnection via onclose if not completed\n } else {\n connect();\n }\n }\n // #endregion handleFilterChange:Function\n" + }, { "contract_id": "fetchTargetDatabases:Function", "contract_type": "Function", @@ -76868,7 +83370,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n {#each $toasts as toast (toast.id)}\n
\n {toast.message}\n
\n {/each}\n
\n\n\n\n" + "body": "\n\n\n\n\n\n\n
\n {#each $toasts as toast (toast.id)}\n
\n {toast.message}\n
\n {/each}\n
\n\n\n\n" }, { "contract_id": "TaskLogViewerTest:Module", @@ -77352,7 +83854,7 @@ "contract_type": "Function", "file_path": "frontend/src/components/git/CommitModal.svelte", "start_line": 37, - "end_line": 60, + "end_line": 62, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -77362,14 +83864,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleGenerateMessage:Function [TYPE Function]\n /**\n * @purpose Generates a commit message using LLM.\n */\n async function handleGenerateMessage() {\n generatingMessage = true;\n try {\n console.log(\n `[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`,\n );\n // postApi returns the JSON data directly or throws an error\n const data = await api.postApi(\n `/git/repositories/${encodeURIComponent(String(dashboardId))}/generate-message${envId ? `?env_id=${encodeURIComponent(String(envId))}` : \"\"}`,\n );\n message = data.message;\n toast($t.git?.commit_message_generated, \"success\");\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast(e.message || $t.git?.commit_message_failed, \"error\");\n } finally {\n generatingMessage = false;\n }\n }\n // #endregion handleGenerateMessage:Function\n" + "body": " // #region handleGenerateMessage:Function [TYPE Function]\n /**\n * @purpose Generates a commit message using LLM.\n */\n async function handleGenerateMessage() {\n generatingMessage = true;\n try {\n console.log(\n `[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`,\n );\n // postApi returns the JSON data directly or throws an error\n const data = await api.postApi(\n `/git/repositories/${encodeURIComponent(String(dashboardId))}/generate-message${envId ? `?env_id=${encodeURIComponent(String(envId))}` : \"\"}`,\n undefined,\n { suppressToast: true },\n );\n message = data.message;\n toast($t.git?.commit_message_generated, \"success\");\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast(e.message || $t.git?.commit_message_failed, \"error\");\n } finally {\n generatingMessage = false;\n }\n }\n // #endregion handleGenerateMessage:Function\n" }, { "contract_id": "loadStatus:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/CommitModal.svelte", - "start_line": 62, - "end_line": 103, + "start_line": 64, + "end_line": 105, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -77396,8 +83898,8 @@ "contract_id": "handleCommit:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/CommitModal.svelte", - "start_line": 105, - "end_line": 130, + "start_line": 107, + "end_line": 132, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -77588,603 +84090,563 @@ "body": " // #region handleDeploy:Function [TYPE Function]\n /**\n * @purpose Trigger deployment to selected environment.\n * @pre selectedEnv must be set.\n * @post Deploy request finishes and modal closes on success.\n * @side_effect Triggers API call, closes modal, shows toast.\n */\n async function handleDeploy() {\n if (!selectedEnv) return;\n if (isProdTarget) {\n const expected = String(dashboardId);\n const confirmation = prompt(`Подтвердите deploy в PROD. Введите slug: ${expected}`);\n if (String(confirmation || \"\").trim() !== expected) {\n toast(\"Подтверждение PROD не пройдено. Deploy отменен.\", \"error\");\n return;\n }\n }\n console.log(`[DeploymentModal][Action] Deploying to ${selectedEnv}`);\n deploying = true;\n try {\n const result = await gitService.deploy(dashboardId, selectedEnv, envId);\n toast(\n result.message || $t.git?.deploy_success,\n \"success\",\n );\n show = false;\n console.log(`[DeploymentModal][Coherence:OK] Deployment triggered`);\n } catch (e) {\n console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);\n toast(e.message, \"error\");\n } finally {\n deploying = false;\n }\n }\n // #endregion handleDeploy:Function\n" }, { - "contract_id": "stageBadgeClass:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 112, - "end_line": 122, - "tier": "TIER_1", - "complexity": 1, + "contract_id": "GitInitPanel", + "contract_type": "Component", + "file_path": "frontend/src/components/git/GitInitPanel.svelte", + "start_line": 1, + "end_line": 65, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "POST": "Stage badge is color-coded by risk level.", - "PURPOSE": "Return visual class for environment stage badges." + "COMPLEXITY": 3, + "LAYER": "UI", + "PURPOSE": "Git initialization panel: select config, create remote repo, init local repo.", + "UX_STATE": "Loading -> Buttons disabled, spinner visible." }, - "relations": [], + "relations": [ + { + "source_id": "GitInitPanel", + "relation_type": "DEPENDS_ON", + "target_id": "GitUtils", + "target_ref": "[GitUtils]" + }, + { + "source_id": "GitInitPanel", + "relation_type": "CALLS", + "target_id": "gitService", + "target_ref": "[gitService]" + } + ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region stageBadgeClass:Function [TYPE Function]\n /**\n * @purpose Return visual class for environment stage badges.\n * @post Stage badge is color-coded by risk level.\n */\n function stageBadgeClass(stage) {\n if (stage === 'PROD') return 'bg-red-100 text-red-800 border-red-200';\n if (stage === 'PREPROD') return 'bg-amber-100 text-amber-800 border-amber-200';\n return 'bg-blue-100 text-blue-800 border-blue-200';\n }\n // #endregion stageBadgeClass:Function\n" + "body": "\n\n\n\n\n\n\n\n\n
\n \n

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

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

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

\n {/if}\n\n \n \n Create repo\n \n\n \n {$t.git?.init_repo || 'Инициализировать Git-репозиторий'}\n \n
\n
\n
\n\n" }, { - "contract_id": "resolveCurrentEnvironmentId:Function", - "contract_type": "Function", + "contract_id": "GitManager", + "contract_type": "Component", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 124, - "end_line": 134, - "tier": "TIER_1", - "complexity": 1, + "start_line": 1, + "end_line": 228, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "POST": "Returns env id from prop or selected_env_id in localStorage.", - "PURPOSE": "Resolve active environment id for current dashboard session." + "COMPLEXITY": 4, + "LAYER": "UI", + "PURPOSE": "Central Git management UI — thin shell delegating to sub-panels and composable handlers.", + "RATIONALE": "Decomposed from 1220→~370 lines by extracting sub-panels, utils, and composable handlers per INV_7.", + "REJECTED": "Keeping all logic inline was rejected because it exceeded 150-line contract limit.", + "UX_STATE": "MergeDialog -> Recovery overlay." }, - "relations": [], + "relations": [ + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "GitInitPanel", + "target_ref": "[GitInitPanel]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "GitWorkspacePanel", + "target_ref": "[GitWorkspacePanel]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "GitReleasePanel", + "target_ref": "[GitReleasePanel]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "GitOperationsPanel", + "target_ref": "[GitOperationsPanel]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "GitMergeDialog", + "target_ref": "[GitMergeDialog]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "BranchSelector", + "target_ref": "[BranchSelector]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "DeploymentModal", + "target_ref": "[DeploymentModal]" + }, + { + "source_id": "GitManager", + "relation_type": "USES", + "target_id": "ConflictResolver", + "target_ref": "[ConflictResolver]" + }, + { + "source_id": "GitManager", + "relation_type": "CALLS", + "target_id": "gitService", + "target_ref": "[gitService]" + }, + { + "source_id": "GitManager", + "relation_type": "DEPENDS_ON", + "target_id": "GitUtils", + "target_ref": "[GitUtils]" + }, + { + "source_id": "GitManager", + "relation_type": "DEPENDS_ON", + "target_id": "UseGitManager", + "target_ref": "[UseGitManager]" + } + ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" + } + }, + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate USES is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "USES" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region resolveCurrentEnvironmentId:Function [TYPE Function]\n /**\n * @purpose Resolve active environment id for current dashboard session.\n * @post Returns env id from prop or selected_env_id in localStorage.\n */\n function resolveCurrentEnvironmentId() {\n if (envId) return String(envId);\n if (typeof window === 'undefined') return null;\n return localStorage.getItem('selected_env_id');\n }\n // #endregion resolveCurrentEnvironmentId:Function\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{#if show}\n
{ if (e.key === 'Escape') closeModal(); }} role=\"button\" tabindex=\"0\" aria-label={$t.common?.close || 'Close'}>\n
\n \n \n
slug: {dashboardId}
\n
\n\n {#if checkingStatus}\n
\n {:else if !initialized}\n \n {:else}\n
\n {#if hasOriginConfigMismatch}\n
Git server mismatch detected
Configured: {configHost}, origin: {originHost}.
\n {/if}\n
\n
\n {currentEnvStage || 'DEV'}\n Текущая ветка: {currentBranch}\n
\n
\n
\n
\n \n \n \n
\n {#if activeTab === 'workspace'}\n \n {:else if activeTab === 'release'}\n \n {:else}\n \n {/if}\n
\n {/if}\n
\n \n
\n{/if}\n\n\n\n" }, { - "contract_id": "applyGitflowStageDefaults:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 136, - "end_line": 159, - "tier": "TIER_1", - "complexity": 1, + "contract_id": "GitMergeDialog", + "contract_type": "Component", + "file_path": "frontend/src/components/git/GitMergeDialog.svelte", + "start_line": 1, + "end_line": 131, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "POST": "Promote branches and preferred deploy target stage are set.", - "PURPOSE": "Apply GitFlow defaults by current environment stage." + "COMPLEXITY": 3, + "LAYER": "UI", + "PURPOSE": "Unfinished merge recovery dialog: abort, continue, resolve conflicts.", + "UX_STATE": "Loading -> Recovery state loading spinner." }, - "relations": [], + "relations": [ + { + "source_id": "GitMergeDialog", + "relation_type": "DEPENDS_ON", + "target_id": "GitUtils", + "target_ref": "[GitUtils]" + }, + { + "source_id": "GitMergeDialog", + "relation_type": "CALLS", + "target_id": "gitService", + "target_ref": "[gitService]" + } + ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region applyGitflowStageDefaults:Function [TYPE Function]\n /**\n * @purpose Apply GitFlow defaults by current environment stage.\n * @post Promote branches and preferred deploy target stage are set.\n */\n function applyGitflowStageDefaults(stage) {\n const normalizedStage = String(stage || '').toUpperCase();\n if (normalizedStage === 'DEV') {\n promoteFromBranch = 'dev';\n promoteToBranch = 'preprod';\n preferredDeployTargetStage = 'PREPROD';\n return;\n }\n if (normalizedStage === 'PREPROD') {\n promoteFromBranch = 'preprod';\n promoteToBranch = 'main';\n preferredDeployTargetStage = 'PROD';\n return;\n }\n promoteFromBranch = 'main';\n promoteToBranch = 'main';\n preferredDeployTargetStage = '';\n }\n // #endregion applyGitflowStageDefaults:Function\n" + "body": "\n\n\n\n\n\n\n\n\n{#if show && unfinishedMergeContext}\n \n
\n
\n {$t.git?.unfinished_merge?.title || 'Repository has an unfinished merge'}\n
\n

\n {unfinishedMergeContext.message || ($t.git?.unfinished_merge?.default_message || 'An unfinished merge was detected in this repository.')}\n

\n\n
\n
\n
{$t.git?.unfinished_merge?.repository_path || 'Repository path'}
\n
{unfinishedMergeContext.repositoryPath || '—'}
\n
\n
\n
{$t.git?.unfinished_merge?.branch || 'Current branch'}
\n
{unfinishedMergeContext.currentBranch || '—'}
\n
\n {#if unfinishedMergeContext.mergeHead}\n
\n
MERGE_HEAD
\n
{unfinishedMergeContext.mergeHead}
\n
\n {/if}\n
\n\n {#if unfinishedMergeContext.nextSteps?.length}\n
\n
\n {$t.git?.unfinished_merge?.next_steps || 'Recommended steps'}\n
\n
    \n {#each unfinishedMergeContext.nextSteps as step}\n
  1. {step}
  2. \n {/each}\n
\n
\n {/if}\n\n {#if unfinishedMergeContext.commands?.length}\n
\n
\n {$t.git?.unfinished_merge?.manual_commands || 'Manual recovery commands'}\n
\n
{unfinishedMergeContext.commands.join('\\n')}
\n
\n {/if}\n\n
\n \n {$t.common?.refresh || 'Refresh'}\n \n \n {$t.git?.unfinished_merge?.copy_commands || 'Copy commands'}\n \n \n {$t.git?.unfinished_merge?.open_resolver || 'Open conflict resolver'}\n \n \n {$t.git?.unfinished_merge?.abort_merge || 'Abort merge'}\n \n \n {$t.git?.unfinished_merge?.continue_merge || 'Continue merge'}\n \n \n
\n
\n \n{/if}\n\n" }, { - "contract_id": "loadCurrentEnvironmentStage:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 161, - "end_line": 179, + "contract_id": "GitOperationsPanel", + "contract_type": "Component", + "file_path": "frontend/src/components/git/GitOperationsPanel.svelte", + "start_line": 1, + "end_line": 46, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "POST": "currentEnvStage and promote/deploy defaults are refreshed.", - "PURPOSE": "Detect current environment stage and bind GitFlow defaults." + "COMPLEXITY": 2, + "LAYER": "UI", + "PURPOSE": "Git server operations panel: pull, push, and deploy actions." }, "relations": [], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region loadCurrentEnvironmentStage:Function [TYPE Function]\n /**\n * @purpose Detect current environment stage and bind GitFlow defaults.\n * @post currentEnvStage and promote/deploy defaults are refreshed.\n */\n async function loadCurrentEnvironmentStage() {\n try {\n const currentEnvId = resolveCurrentEnvironmentId();\n if (!currentEnvId) return;\n const environments = await api.getEnvironmentsList();\n const currentEnv = (environments || []).find((item) => item.id === currentEnvId);\n if (!currentEnv) return;\n currentEnvStage = normalizeEnvStage(currentEnv);\n applyGitflowStageDefaults(currentEnvStage);\n } catch (e) {\n console.error(`[GitManager][Coherence:Failed] Failed to resolve environment stage: ${e.message}`);\n }\n }\n // #endregion loadCurrentEnvironmentStage:Function\n" + "body": "\n\n\n\n\n
\n \n ⬇ Pull\n \n \n ⬆ Push{#if workspaceStatus?.ahead_count > 0} ({workspaceStatus.ahead_count}){/if}\n \n \n 🚀 Deploy\n \n
\n\n" }, { - "contract_id": "isNumericDashboardRef:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 181, - "end_line": 189, - "tier": "TIER_1", - "complexity": 1, + "contract_id": "GitReleasePanel", + "contract_type": "Component", + "file_path": "frontend/src/components/git/GitReleasePanel.svelte", + "start_line": 1, + "end_line": 97, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "POST": "Returns true when dashboardId is digits-only.", - "PURPOSE": "Checks whether current dashboard reference is numeric ID." + "COMPLEXITY": 3, + "LAYER": "UI", + "PURPOSE": "Git release panel: promote branches via MR or direct mode.", + "UX_STATE": "Advanced -> Expandable branch/reason settings." }, - "relations": [], + "relations": [ + { + "source_id": "GitReleasePanel", + "relation_type": "DEPENDS_ON", + "target_id": "GitUtils", + "target_ref": "[GitUtils]" + }, + { + "source_id": "GitReleasePanel", + "relation_type": "CALLS", + "target_id": "gitService", + "target_ref": "[gitService]" + } + ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region isNumericDashboardRef:Function [TYPE Function]\n /**\n * @purpose Checks whether current dashboard reference is numeric ID.\n * @post Returns true when dashboardId is digits-only.\n */\n function isNumericDashboardRef() {\n return /^\\d+$/.test(String(dashboardId || '').trim());\n }\n // #endregion isNumericDashboardRef:Function\n" + "body": "\n\n\n\n\n\n\n\n\n
\n
\n
Текущий статус пайплайна
\n
\n DEV (dev)\n \n PREPROD (preprod)\n \n PROD (main)\n
\n {#if preferredDeployTargetStage}\n

\n Следующий шаг по GitFlow: {promoteFromBranch} ➔ {promoteToBranch}\n

\n {/if}\n
\n\n \n {promoteMode === 'direct'\n ? `Прямой перенос ${promoteFromBranch} ➔ ${promoteToBranch} (unsafe)`\n : `Создать Merge Request (${promoteFromBranch} ➔ ${promoteToBranch})`}\n \n\n (showAdvancedPromote = !showAdvancedPromote)}\n >\n {showAdvancedPromote ? '▴ Скрыть advanced settings' : '▾ Advanced settings'}\n \n\n {#if showAdvancedPromote}\n
\n
\n \n \n
\n \n {#if promoteMode === 'direct'}\n
\n
Внимание: прямой перенос без MR
\n
Это обходит процесс аппрува и записывается в audit лог.
\n
\n \n {/if}\n
\n {/if}\n
\n\n" }, { - "contract_id": "checkStatus:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 191, - "end_line": 217, - "tier": "TIER_1", - "complexity": 1, + "contract_id": "GitWorkspacePanel", + "contract_type": "Component", + "file_path": "frontend/src/components/git/GitWorkspacePanel.svelte", + "start_line": 1, + "end_line": 96, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "POST": "initialized status and config list are updated.", - "PURPOSE": "Verify repository initialization for dashboard slug." + "COMPLEXITY": 3, + "LAYER": "UI", + "PURPOSE": "Git workspace panel: sync, commit message input, diff viewer, commit action.", + "UX_STATE": "Error -> Toast on failure." }, - "relations": [], + "relations": [ + { + "source_id": "GitWorkspacePanel", + "relation_type": "DEPENDS_ON", + "target_id": "GitUtils", + "target_ref": "[GitUtils]" + }, + { + "source_id": "GitWorkspacePanel", + "relation_type": "CALLS", + "target_id": "gitService", + "target_ref": "[gitService]" + } + ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region checkStatus:Function [TYPE Function]\n /**\n * @purpose Verify repository initialization for dashboard slug.\n * @post initialized status and config list are updated.\n */\n async function checkStatus() {\n if (isNumericDashboardRef()) {\n checkingStatus = false;\n initialized = false;\n toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n checkingStatus = true;\n try {\n await gitService.getBranches(dashboardId, envId);\n initialized = true;\n await loadWorkspace();\n } catch (_e) {\n initialized = false;\n configs = await gitService.getConfigs();\n const defaultConfig = resolveDefaultConfig(configs);\n if (defaultConfig?.id) selectedConfigId = defaultConfig.id;\n } finally {\n checkingStatus = false;\n }\n }\n // #endregion checkStatus:Function\n" - }, - { - "contract_id": "loadWorkspace:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 219, - "end_line": 239, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "workspace status/diff are refreshed.", - "PURPOSE": "Load current git status and combined diff for workspace tab." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region loadWorkspace:Function [TYPE Function]\n /**\n * @purpose Load current git status and combined diff for workspace tab.\n * @post workspace status/diff are refreshed.\n */\n async function loadWorkspace() {\n if (!initialized) return;\n workspaceLoading = true;\n try {\n workspaceStatus = await gitService.getStatus(dashboardId, envId);\n const stagedDiff = await gitService.getDiff(dashboardId, null, true, envId);\n const unstagedDiff = await gitService.getDiff(dashboardId, null, false, envId);\n workspaceDiff = [stagedDiff, unstagedDiff].filter(Boolean).join('\\n\\n');\n currentBranch = workspaceStatus?.current_branch || currentBranch;\n } catch (e) {\n toast(e.message || 'Не удалось загрузить изменения', 'error');\n } finally {\n workspaceLoading = false;\n }\n }\n // #endregion loadWorkspace:Function\n" - }, - { - "contract_id": "handleSync:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 241, - "end_line": 263, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Workspace status is refreshed.", - "PURPOSE": "Sync latest dashboard config from Superset to git workspace." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handleSync:Function [TYPE Function]\n /**\n * @purpose Sync latest dashboard config from Superset to git workspace.\n * @post Workspace status is refreshed.\n */\n async function handleSync() {\n if (isNumericDashboardRef()) {\n toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n loading = true;\n try {\n const sourceEnvId = localStorage.getItem('selected_env_id');\n await gitService.sync(dashboardId, sourceEnvId, envId);\n toast($t.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');\n await loadWorkspace();\n } catch (e) {\n toast(e.message, 'error');\n } finally {\n loading = false;\n }\n }\n // #endregion handleSync:Function\n" - }, - { - "contract_id": "handlePull:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 312, - "end_line": 509, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Workspace status is refreshed.", - "PURPOSE": "Pull remote changes for current branch." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handlePull:Function [TYPE Function]\n /**\n * @purpose Pull remote changes for current branch.\n * @post Workspace status is refreshed.\n */\n function tryParseJsonObject(value) {\n const source = String(value || '').trim();\n if (!source.startsWith('{') || !source.endsWith('}')) return null;\n try {\n const parsed = JSON.parse(source);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;\n } catch (_e) {\n return null;\n }\n }\n\n function extractUnfinishedMergeContext(error) {\n if (!error || Number(error?.status) !== 409) return null;\n const parsedMessage = tryParseJsonObject(error?.message);\n const detail = error?.detail && typeof error.detail === 'object' ? error.detail : null;\n const payload = detail || parsedMessage;\n if (!payload || payload.error_code !== 'GIT_UNFINISHED_MERGE') return null;\n\n const commands = Array.isArray(payload.manual_commands)\n ? payload.manual_commands.filter(Boolean).map((item) => String(item))\n : [];\n\n const nextSteps = Array.isArray(payload.next_steps)\n ? payload.next_steps.filter(Boolean).map((item) => String(item))\n : [];\n\n return {\n message: String(payload.message || ''),\n repositoryPath: String(payload.repository_path || ''),\n gitDir: String(payload.git_dir || ''),\n currentBranch: String(payload.current_branch || ''),\n mergeHead: String(payload.merge_head || ''),\n mergeMessagePreview: String(payload.merge_message_preview || ''),\n nextSteps,\n commands,\n };\n }\n\n function openUnfinishedMergeDialogFromError(error) {\n const context = extractUnfinishedMergeContext(error);\n if (!context) return false;\n unfinishedMergeContext = context;\n showUnfinishedMergeDialog = true;\n return true;\n }\n\n async function loadMergeRecoveryState() {\n mergeRecoveryLoading = true;\n try {\n const status = await gitService.getMergeStatus(dashboardId, envId);\n if (!status?.has_unfinished_merge) {\n closeUnfinishedMergeDialog();\n return;\n }\n unfinishedMergeContext = {\n ...(unfinishedMergeContext || {}),\n message: unfinishedMergeContext?.message || ($t.git?.unfinished_merge?.default_message || ''),\n repositoryPath: String(status.repository_path || unfinishedMergeContext?.repositoryPath || ''),\n gitDir: String(status.git_dir || unfinishedMergeContext?.gitDir || ''),\n currentBranch: String(status.current_branch || unfinishedMergeContext?.currentBranch || ''),\n mergeHead: String(status.merge_head || unfinishedMergeContext?.mergeHead || ''),\n mergeMessagePreview: String(status.merge_message_preview || unfinishedMergeContext?.mergeMessagePreview || ''),\n nextSteps: Array.isArray(unfinishedMergeContext?.nextSteps) ? unfinishedMergeContext.nextSteps : [],\n commands: Array.isArray(unfinishedMergeContext?.commands) ? unfinishedMergeContext.commands : [],\n conflictsCount: Number(status.conflicts_count || 0),\n };\n } catch (e) {\n toast(e.message || ($t.git?.unfinished_merge?.load_status_failed || 'Failed to load merge status'), 'error');\n } finally {\n mergeRecoveryLoading = false;\n }\n }\n\n function closeUnfinishedMergeDialog() {\n showUnfinishedMergeDialog = false;\n unfinishedMergeContext = null;\n mergeConflicts = [];\n showConflictResolver = false;\n }\n\n async function handleOpenConflictResolver() {\n mergeRecoveryLoading = true;\n try {\n mergeConflicts = await gitService.getMergeConflicts(dashboardId, envId);\n if (!Array.isArray(mergeConflicts) || mergeConflicts.length === 0) {\n toast($t.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info');\n return;\n }\n showConflictResolver = true;\n } catch (e) {\n toast(e.message || ($t.git?.unfinished_merge?.load_conflicts_failed || 'Failed to load merge conflicts'), 'error');\n } finally {\n mergeRecoveryLoading = false;\n }\n }\n\n async function handleResolveConflicts(event) {\n const detail = event?.detail || {};\n const resolutions = Object.entries(detail).map(([file_path, resolution]) => ({\n file_path,\n resolution,\n }));\n if (!resolutions.length) {\n toast($t.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning');\n return;\n }\n\n mergeResolveInProgress = true;\n try {\n await gitService.resolveMergeConflicts(dashboardId, resolutions, envId);\n toast($t.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');\n showConflictResolver = false;\n await loadMergeRecoveryState();\n await loadWorkspace();\n } catch (e) {\n toast(e.message || ($t.git?.unfinished_merge?.resolve_failed || 'Failed to resolve conflicts'), 'error');\n } finally {\n mergeResolveInProgress = false;\n }\n }\n\n async function handleAbortUnfinishedMerge() {\n mergeAbortInProgress = true;\n try {\n await gitService.abortMerge(dashboardId, envId);\n toast($t.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');\n closeUnfinishedMergeDialog();\n await loadWorkspace();\n } catch (e) {\n toast(e.message || ($t.git?.unfinished_merge?.abort_failed || 'Failed to abort merge'), 'error');\n } finally {\n mergeAbortInProgress = false;\n }\n }\n\n async function handleContinueUnfinishedMerge() {\n mergeContinueInProgress = true;\n try {\n await gitService.continueMerge(dashboardId, '', envId);\n toast($t.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');\n closeUnfinishedMergeDialog();\n await loadWorkspace();\n } catch (e) {\n toast(e.message || ($t.git?.unfinished_merge?.continue_failed || 'Failed to continue merge'), 'error');\n await loadMergeRecoveryState();\n } finally {\n mergeContinueInProgress = false;\n }\n }\n\n function getUnfinishedMergeCommandsText() {\n if (!unfinishedMergeContext?.commands?.length) return '';\n return unfinishedMergeContext.commands.join('\\n');\n }\n\n async function handleCopyUnfinishedMergeCommands() {\n const commandsText = getUnfinishedMergeCommandsText();\n if (!commandsText) {\n toast($t.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning');\n return;\n }\n copyingUnfinishedMergeCommands = true;\n try {\n if (typeof navigator === 'undefined' || !navigator?.clipboard?.writeText) {\n throw new Error('Clipboard API unavailable');\n }\n await navigator.clipboard.writeText(commandsText);\n toast($t.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');\n } catch (_e) {\n toast($t.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error');\n } finally {\n copyingUnfinishedMergeCommands = false;\n }\n }\n\n async function handlePull() {\n isPulling = true;\n try {\n await gitService.pull(dashboardId, envId);\n toast($t.git?.pull_success || 'Изменения получены из Git', 'success');\n await loadWorkspace();\n } catch (e) {\n const handledByDialog = openUnfinishedMergeDialogFromError(e);\n if (handledByDialog) {\n await loadMergeRecoveryState();\n } else {\n toast(e.message, 'error');\n }\n } finally {\n isPulling = false;\n }\n }\n // #endregion handlePull:Function\n" - }, - { - "contract_id": "handlePush:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 511, - "end_line": 528, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Workspace status is refreshed.", - "PURPOSE": "Push local commits to remote repository." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handlePush:Function [TYPE Function]\n /**\n * @purpose Push local commits to remote repository.\n * @post Workspace status is refreshed.\n */\n async function handlePush() {\n isPushing = true;\n try {\n await gitService.push(dashboardId, envId);\n toast($t.git?.push_success || 'Изменения отправлены в Git', 'success');\n await loadWorkspace();\n } catch (e) {\n toast(e.message, 'error');\n } finally {\n isPushing = false;\n }\n }\n // #endregion handlePush:Function\n" - }, - { - "contract_id": "handlePromote:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 530, - "end_line": 578, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Promotion result is shown to user.", - "PURPOSE": "Promote branch to the next stage via MR or unsafe direct mode." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handlePromote:Function [TYPE Function]\n /**\n * @purpose Promote branch to the next stage via MR or unsafe direct mode.\n * @post Promotion result is shown to user.\n */\n async function handlePromote() {\n const fromBranch = String(promoteFromBranch || '').trim();\n const toBranch = String(promoteToBranch || '').trim();\n if (!fromBranch || !toBranch || fromBranch === toBranch) {\n toast('Выберите разные исходную и целевую ветки', 'error');\n return;\n }\n if (promoteMode === 'direct' && !String(promoteReason || '').trim()) {\n toast('Для небезопасного прямого переноса укажите причину', 'error');\n return;\n }\n\n promoting = true;\n try {\n const response = await gitService.promote(\n dashboardId,\n {\n from_branch: fromBranch,\n to_branch: toBranch,\n mode: promoteMode,\n title: `Promote ${fromBranch} -> ${toBranch}: ${dashboardTitle || dashboardId}`,\n description: promoteMode === 'direct'\n ? `Unsafe direct promote requested.\\nReason: ${promoteReason}`\n : undefined,\n reason: promoteMode === 'direct' ? promoteReason : undefined,\n },\n envId,\n );\n\n if (promoteMode === 'direct') {\n toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning');\n } else {\n if (response?.url) {\n window.open(response.url, '_blank', 'noopener,noreferrer');\n }\n toast('Merge Request создан на Git сервере', 'success');\n }\n } catch (e) {\n toast(e.message, 'error');\n } finally {\n promoting = false;\n }\n }\n // #endregion handlePromote:Function\n" - }, - { - "contract_id": "openDeployModal:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 580, - "end_line": 596, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Deploy modal is shown when confirmation passes.", - "PURPOSE": "Open deploy modal with extra confirmation for PROD context." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region openDeployModal:Function [TYPE Function]\n /**\n * @purpose Open deploy modal with extra confirmation for PROD context.\n * @post Deploy modal is shown when confirmation passes.\n */\n function openDeployModal() {\n if (currentEnvStage === 'PROD') {\n const expected = String(dashboardId);\n const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);\n if (String(confirmation || '').trim() !== expected) {\n toast('Подтверждение PROD не пройдено. Деплой отменен.', 'error');\n return;\n }\n }\n showDeployModal = true;\n }\n // #endregion openDeployModal:Function\n" - }, - { - "contract_id": "getSelectedConfig:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 598, - "end_line": 606, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Config object or null.", - "PURPOSE": "Return currently selected git server config." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region getSelectedConfig:Function [TYPE Function]\n /**\n * @purpose Return currently selected git server config.\n * @post Config object or null.\n */\n function getSelectedConfig() {\n return configs.find((item) => item.id === selectedConfigId) || null;\n }\n // #endregion getSelectedConfig:Function\n" - }, - { - "contract_id": "resolveDefaultConfig:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 608, - "end_line": 622, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns config by selected -> is_default -> CONNECTED -> first.", - "PURPOSE": "Resolve default git config for current session." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region resolveDefaultConfig:Function [TYPE Function]\n /**\n * @purpose Resolve default git config for current session.\n * @post Returns config by selected -> is_default -> CONNECTED -> first.\n */\n function resolveDefaultConfig(configList) {\n if (!Array.isArray(configList) || configList.length === 0) return null;\n const selected = configList.find((item) => item.id === selectedConfigId);\n if (selected) return selected;\n const explicitDefault = configList.find((item) => item?.is_default);\n if (explicitDefault) return explicitDefault;\n const connected = configList.find((item) => item?.status === 'CONNECTED');\n return connected || configList[0];\n }\n // #endregion resolveDefaultConfig:Function\n" - }, - { - "contract_id": "resolvePushProviderLabel:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 624, - "end_line": 636, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns provider label, fallback \"git\".", - "PURPOSE": "Resolve lower-case provider label for auto-push checkbox." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region resolvePushProviderLabel:Function [TYPE Function]\n /**\n * @purpose Resolve lower-case provider label for auto-push checkbox.\n * @post Returns provider label, fallback \"git\".\n */\n function resolvePushProviderLabel() {\n const selectedConfig = getSelectedConfig() || resolveDefaultConfig(configs);\n const provider = String(selectedConfig?.provider || repositoryProvider || '')\n .trim()\n .toLowerCase();\n return provider || 'git';\n }\n // #endregion resolvePushProviderLabel:Function\n" - }, - { - "contract_id": "extractHttpHost:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 638, - "end_line": 654, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns lowercase host token or empty string.", - "PURPOSE": "Extract comparable host[:port] from URL string." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region extractHttpHost:Function [TYPE Function]\n /**\n * @purpose Extract comparable host[:port] from URL string.\n * @post Returns lowercase host token or empty string.\n */\n function extractHttpHost(urlValue) {\n const normalized = String(urlValue || '').trim();\n if (!normalized) return '';\n try {\n const parsed = new URL(normalized);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';\n return String(parsed.host || '').toLowerCase();\n } catch (_e) {\n return '';\n }\n }\n // #endregion extractHttpHost:Function\n" - }, - { - "contract_id": "buildSuggestedRepoName:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 656, - "end_line": 669, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns kebab-case repository name.", - "PURPOSE": "Build deterministic repository name from dashboard title/id." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region buildSuggestedRepoName:Function [TYPE Function]\n /**\n * @purpose Build deterministic repository name from dashboard title/id.\n * @post Returns kebab-case repository name.\n */\n function buildSuggestedRepoName() {\n const source = (dashboardTitle || `dashboard-${dashboardId}`).toLowerCase();\n const slug = source\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48);\n return `${slug || 'dashboard'}-${dashboardId}`;\n }\n // #endregion buildSuggestedRepoName:Function\n" - }, - { - "contract_id": "handleCreateRemoteRepo:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 671, - "end_line": 709, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "remoteUrl is filled from provider response.", - "PURPOSE": "Create remote repository on selected Git provider." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handleCreateRemoteRepo:Function [TYPE Function]\n /**\n * @purpose Create remote repository on selected Git provider.\n * @post remoteUrl is filled from provider response.\n */\n async function handleCreateRemoteRepo() {\n const config = getSelectedConfig() || resolveDefaultConfig(configs);\n if (!config) {\n toast($t.git?.init_validation_error || 'Сначала выберите Git сервер', 'error');\n return;\n }\n if (!selectedConfigId && config.id) selectedConfigId = config.id;\n const suggestedName = buildSuggestedRepoName();\n const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);\n const repoName = String(inputName || '').trim();\n if (!repoName) return;\n\n creatingRemoteRepo = true;\n try {\n const repo = await gitService.createRemoteRepository(config.id, {\n name: repoName,\n private: true,\n description: `Superset dashboard ${dashboardId}: ${dashboardTitle || repoName}`,\n auto_init: true,\n default_branch: 'main',\n });\n const resolvedRemoteUrl = repo?.clone_url || repo?.html_url || '';\n if (!resolvedRemoteUrl) {\n throw new Error('Remote repository created, but URL is empty');\n }\n remoteUrl = resolvedRemoteUrl;\n toast(`Repository created on ${config.provider}`, 'success');\n } catch (e) {\n toast(e.message, 'error');\n } finally {\n creatingRemoteRepo = false;\n }\n }\n // #endregion handleCreateRemoteRepo:Function\n" - }, - { - "contract_id": "handleInit:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 711, - "end_line": 735, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Repository is linked and workspace is loaded.", - "PURPOSE": "Initialize git repository for dashboard." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handleInit:Function [TYPE Function]\n /**\n * @purpose Initialize git repository for dashboard.\n * @post Repository is linked and workspace is loaded.\n */\n async function handleInit() {\n if (!selectedConfigId || !remoteUrl) {\n toast($t.git?.init_validation_error || 'Заполните все поля', 'error');\n return;\n }\n loading = true;\n try {\n await gitService.initRepository(dashboardId, selectedConfigId, remoteUrl, envId);\n toast($t.git?.init_success || 'Репозиторий инициализирован', 'success');\n initialized = true;\n const selectedConfig = getSelectedConfig();\n repositoryProvider = selectedConfig?.provider || repositoryProvider;\n await loadWorkspace();\n } catch (e) {\n toast(e.message, 'error');\n } finally {\n loading = false;\n }\n }\n // #endregion handleInit:Function\n" - }, - { - "contract_id": "closeModal:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 737, - "end_line": 745, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "show=false.", - "PURPOSE": "Close git manager modal." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region closeModal:Function [TYPE Function]\n /**\n * @purpose Close git manager modal.\n * @post show=false.\n */\n function closeModal() {\n show = false;\n }\n // #endregion closeModal:Function\n" - }, - { - "contract_id": "handleBackdropClick:Function", - "contract_type": "Function", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 747, - "end_line": 757, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "show=false if clicked on overlay.", - "PURPOSE": "Close modal on backdrop click." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": " // #region handleBackdropClick:Function [TYPE Function]\n /**\n * @purpose Close modal on backdrop click.\n * @post show=false if clicked on overlay.\n */\n function handleBackdropClick(event) {\n if (event.target === event.currentTarget) {\n closeModal();\n }\n }\n // #endregion handleBackdropClick:Function\n" + "body": "\n\n\n\n\n\n\n\n\n\n
\n
\n \n 🔄 Синхронизировать из Superset\n \n\n
\n
\n

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

\n \n ✨ Сгенерировать LLM\n \n
\n \n
\n\n
\n Файлов с изменениями: {changedFilesCount}\n
\n\n \n Зафиксировать (Commit)\n \n \n
\n\n
\n
\n Diff (изменения)\n
\n
\n {#if workspaceLoading}\n
\n {#each Array(6) as _}\n
\n {/each}\n
\n {:else}\n
\n Нет изменений для коммита\n
\n {/if}\n
\n
\n
\n\n" }, { "contract_id": "GitManagerUnfinishedMergeIntegrationTest:Module", "contract_type": "Function", "file_path": "frontend/src/components/git/__tests__/git_manager.unfinished_merge.integration.test.js", "start_line": 1, - "end_line": 42, + "end_line": 43, "tier": "TIER_1", "complexity": 1, "metadata": { - "LAYER": "UI Tests", "PURPOSE": "Protect unresolved-merge dialog contract in GitManager pull flow.", "SEMANTICS": [ "git-manager", @@ -78202,45 +84664,6 @@ } ], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "LAYER", - "message": "@LAYER is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'UI Tests' is not in allowed enum", - "detail": { - "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "UI Tests" - } - }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", @@ -78276,7 +84699,39 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region GitManagerUnfinishedMergeIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test\n// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [GitManager]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst COMPONENT_PATH = path.resolve(\n process.cwd(),\n 'src/components/git/GitManager.svelte',\n);\n\ndescribe('GitManager unfinished merge dialog contract', () => {\n it('keeps 409 unfinished-merge detection and WebUI dialog recovery flow in pull handler', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('Number(error?.status) !== 409');\n expect(source).toContain(\"payload.error_code !== 'GIT_UNFINISHED_MERGE'\");\n expect(source).toContain('function openUnfinishedMergeDialogFromError(error)');\n expect(source).toContain('showUnfinishedMergeDialog = true;');\n expect(source).toContain('const handledByDialog = openUnfinishedMergeDialogFromError(e);');\n expect(source).toContain('await loadMergeRecoveryState();');\n });\n\n it('renders unresolved-merge dialog details and web recovery actions', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('{#if showUnfinishedMergeDialog && unfinishedMergeContext}');\n expect(source).toContain('unfinishedMergeContext.repositoryPath');\n expect(source).toContain('unfinishedMergeContext.currentBranch');\n expect(source).toContain('unfinishedMergeContext.commands.join');\n expect(source).toContain('handleCopyUnfinishedMergeCommands');\n expect(source).toContain('handleOpenConflictResolver');\n expect(source).toContain('handleAbortUnfinishedMerge');\n expect(source).toContain('handleContinueUnfinishedMerge');\n expect(source).toContain('$t.git?.unfinished_merge?.copy_commands');\n });\n});\n// #endregion GitManagerUnfinishedMergeIntegrationTest:Module\n" + "body": "// #region GitManagerUnfinishedMergeIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test\n// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.\n// @RELATION: DEPENDS_ON -> [GitManager]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst MANAGER_PATH = path.resolve(process.cwd(), 'src/components/git/GitManager.svelte');\nconst UTILS_PATH = path.resolve(process.cwd(), 'src/services/git-utils.js');\nconst HANDLER_PATH = path.resolve(process.cwd(), 'src/components/git/useGitManager.js');\nconst DIALOG_PATH = path.resolve(process.cwd(), 'src/components/git/GitMergeDialog.svelte');\n\ndescribe('GitManager unfinished merge dialog contract', () => {\n it('keeps 409 unfinished-merge detection and WebUI dialog recovery flow in pull handler', () => {\n const utils = fs.readFileSync(UTILS_PATH, 'utf-8');\n const handlers = fs.readFileSync(HANDLER_PATH, 'utf-8');\n const manager = fs.readFileSync(MANAGER_PATH, 'utf-8');\n\n expect(utils).toContain(\"error_code !== 'GIT_UNFINISHED_MERGE'\");\n expect(handlers).toContain('showUnfinishedMergeDialog: true');\n expect(handlers).toContain('this.openUnfinishedMergeDialogFromError(e)');\n expect(handlers).toContain('await this.loadMergeRecoveryState();');\n expect(manager).toContain('h.handlePull');\n expect(manager).toContain('h.loadMergeRecoveryState');\n });\n\n it('renders unresolved-merge dialog details and web recovery actions', () => {\n const source = fs.readFileSync(DIALOG_PATH, 'utf-8');\n\n expect(source).toContain('{#if show && unfinishedMergeContext}');\n expect(source).toContain('unfinishedMergeContext.repositoryPath');\n expect(source).toContain('unfinishedMergeContext.currentBranch');\n expect(source).toContain('unfinishedMergeContext.commands.join');\n expect(source).toContain('onCopyCommands');\n expect(source).toContain('onOpenConflictResolver');\n expect(source).toContain('onAbortMerge');\n expect(source).toContain('onContinueMerge');\n expect(source).toContain('$t.git?.unfinished_merge?.copy_commands');\n });\n});\n// #endregion GitManagerUnfinishedMergeIntegrationTest:Module\n" + }, + { + "contract_id": "UseGitManager", + "contract_type": "Module", + "file_path": "frontend/src/components/git/useGitManager.js", + "start_line": 1, + "end_line": 275, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Composable for GitManager handler functions — extracted to reduce component size per INV_7.", + "RATIONALE": "Extracted handler logic from GitManager (1220→~350 lines) to meet INV_7 <150 line contract limit." + }, + "relations": [ + { + "source_id": "UseGitManager", + "relation_type": "DEPENDS_ON", + "target_id": "gitService", + "target_ref": "[gitService]" + }, + { + "source_id": "UseGitManager", + "relation_type": "DEPENDS_ON", + "target_id": "GitUtils", + "target_ref": "[GitUtils]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region UseGitManager [C:3] [TYPE Module] [SEMANTICS git, handlers, composable, orchestration]\n// @BRIEF Composable for GitManager handler functions — extracted to reduce component size per INV_7.\n// @RELATION DEPENDS_ON -> [gitService]\n// @RELATION DEPENDS_ON -> [GitUtils]\n// @RATIONALE Extracted handler logic from GitManager (1220→~350 lines) to meet INV_7 <150 line contract limit.\nimport { gitService } from '../../services/gitService';\nimport { api } from '../../lib/api.js';\nimport { isNumericDashboardRef, resolveDefaultConfig, extractUnfinishedMergeContext, buildSuggestedRepoName } from '../../services/git-utils.js';\n\nexport function createGitHandlers(getState, setState) {\n const s = () => getState();\n const u = (patch) => setState(patch);\n\n return {\n async loadCurrentEnvironmentStage() {\n const currentEnvId = s().resolveEnvId();\n if (!currentEnvId) return;\n try {\n const environments = await api.getEnvironmentsList();\n const currentEnv = (environments || []).find((item) => item.id === currentEnvId);\n if (!currentEnv) return;\n const stage = s().normalizeEnvStageFn(currentEnv);\n u({ currentEnvStage: stage });\n const defaults = s().applyGitflowStageDefaultsFn(stage);\n u(defaults);\n } catch (e) {\n console.error(`[GitManager][Coherence:Failed] Failed to resolve environment stage: ${e.message}`);\n }\n },\n\n async checkStatus() {\n if (isNumericDashboardRef(s().dashboardId)) {\n u({ checkingStatus: false, initialized: false });\n s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n u({ checkingStatus: true });\n try {\n await gitService.getBranches(s().dashboardId, s().envId);\n u({ initialized: true });\n await this.loadWorkspace();\n } catch (_e) {\n u({ initialized: false });\n try { const configs = await gitService.getConfigs(); u({ configs }); } catch (_e2) { u({ configs: [] }); }\n const defaultConfig = resolveDefaultConfig(s().configs, s().selectedConfigId);\n if (defaultConfig?.id) u({ selectedConfigId: defaultConfig.id });\n } finally { u({ checkingStatus: false }); }\n },\n\n async loadWorkspace() {\n if (!s().initialized) return;\n u({ workspaceLoading: true });\n try {\n const ws = await gitService.getStatus(s().dashboardId, s().envId);\n const sd = await gitService.getDiff(s().dashboardId, null, true, s().envId);\n const ud = await gitService.getDiff(s().dashboardId, null, false, s().envId);\n u({ workspaceStatus: ws, workspaceDiff: [sd, ud].filter(Boolean).join('\\n\\n'), currentBranch: ws?.current_branch || s().currentBranch });\n } catch (e) {\n s().toast(e.message || 'Не удалось загрузить изменения', 'error');\n } finally { u({ workspaceLoading: false }); }\n },\n\n async handleSync() {\n if (isNumericDashboardRef(s().dashboardId)) { s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error'); return; }\n u({ loading: true });\n try {\n const sourceEnvId = localStorage.getItem('selected_env_id');\n await gitService.sync(s().dashboardId, sourceEnvId, s().envId);\n s().toast(s().t?.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }\n },\n\n async handleGenerateMessage() {\n u({ generatingMessage: true });\n try {\n const data = await api.postApi(`/git/repositories/${encodeURIComponent(String(s().dashboardId))}/generate-message${s().envId ? `?env_id=${encodeURIComponent(String(s().envId))}` : ''}`, undefined, { suppressToast: true });\n u({ commitMessage: data?.message || '' });\n s().toast(s().t?.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');\n } catch (e) { s().toast(e.message || s().t?.git?.commit_message_failed || 'Не удалось сгенерировать сообщение', 'error'); } finally { u({ generatingMessage: false }); }\n },\n\n async handleCommit() {\n if (!s().commitMessage || !s().hasWorkspaceChanges) return;\n u({ committing: true });\n try {\n await gitService.commit(s().dashboardId, s().commitMessage, [], s().envId);\n if (s().autoPushAfterCommit) {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');\n } else { s().toast(s().t?.git?.commit_success || 'Коммит успешно создан', 'success'); }\n u({ commitMessage: '' });\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ committing: false }); }\n },\n\n openUnfinishedMergeDialogFromError(error) {\n const context = extractUnfinishedMergeContext(error);\n if (!context) return false;\n u({ unfinishedMergeContext: context, showUnfinishedMergeDialog: true });\n return true;\n },\n\n async loadMergeRecoveryState() {\n u({ mergeRecoveryLoading: true });\n try {\n const status = await gitService.getMergeStatus(s().dashboardId, s().envId);\n if (!status?.has_unfinished_merge) { this.closeUnfinishedMergeDialog(); return; }\n u({\n unfinishedMergeContext: {\n ...(s().unfinishedMergeContext || {}),\n message: s().unfinishedMergeContext?.message || (s().t?.git?.unfinished_merge?.default_message || ''),\n repositoryPath: String(status.repository_path || s().unfinishedMergeContext?.repositoryPath || ''),\n gitDir: String(status.git_dir || s().unfinishedMergeContext?.gitDir || ''),\n currentBranch: String(status.current_branch || s().unfinishedMergeContext?.currentBranch || ''),\n mergeHead: String(status.merge_head || s().unfinishedMergeContext?.mergeHead || ''),\n mergeMessagePreview: String(status.merge_message_preview || ''),\n nextSteps: Array.isArray(s().unfinishedMergeContext?.nextSteps) ? s().unfinishedMergeContext.nextSteps : [],\n commands: Array.isArray(s().unfinishedMergeContext?.commands) ? s().unfinishedMergeContext.commands : [],\n conflictsCount: Number(status.conflicts_count || 0),\n }\n });\n } catch (e) { s().toast(e.message || 'Failed to load merge status', 'error'); } finally { u({ mergeRecoveryLoading: false }); }\n },\n\n closeUnfinishedMergeDialog() {\n u({ showUnfinishedMergeDialog: false, unfinishedMergeContext: null, mergeConflicts: [], showConflictResolver: false });\n },\n\n async handleOpenConflictResolver() {\n u({ mergeRecoveryLoading: true });\n try {\n const mc = await gitService.getMergeConflicts(s().dashboardId, s().envId);\n if (!Array.isArray(mc) || mc.length === 0) { s().toast(s().t?.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info'); return; }\n u({ mergeConflicts: mc, showConflictResolver: true });\n } catch (e) { s().toast(e.message || 'Failed to load merge conflicts', 'error'); } finally { u({ mergeRecoveryLoading: false }); }\n },\n\n async handleResolveConflicts(event) {\n const detail = event?.detail || {};\n const resolutions = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));\n if (!resolutions.length) { s().toast(s().t?.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning'); return; }\n u({ mergeResolveInProgress: true });\n try {\n await gitService.resolveMergeConflicts(s().dashboardId, resolutions, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');\n u({ showConflictResolver: false });\n await this.loadMergeRecoveryState();\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message || 'Failed to resolve conflicts', 'error'); } finally { u({ mergeResolveInProgress: false }); }\n },\n\n async handleAbortUnfinishedMerge() {\n u({ mergeAbortInProgress: true });\n try {\n await gitService.abortMerge(s().dashboardId, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');\n this.closeUnfinishedMergeDialog();\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message || 'Failed to abort merge', 'error'); } finally { u({ mergeAbortInProgress: false }); }\n },\n\n async handleContinueUnfinishedMerge() {\n u({ mergeContinueInProgress: true });\n try {\n await gitService.continueMerge(s().dashboardId, '', s().envId);\n s().toast(s().t?.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');\n this.closeUnfinishedMergeDialog();\n await this.loadWorkspace();\n } catch (e) {\n s().toast(e.message || 'Failed to continue merge', 'error');\n await this.loadMergeRecoveryState();\n } finally { u({ mergeContinueInProgress: false }); }\n },\n\n getUnfinishedMergeCommandsText() {\n if (!s().unfinishedMergeContext?.commands?.length) return '';\n return s().unfinishedMergeContext.commands.join('\\n');\n },\n\n async handleCopyUnfinishedMergeCommands() {\n const text = this.getUnfinishedMergeCommandsText();\n if (!text) { s().toast(s().t?.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning'); return; }\n u({ copyingUnfinishedMergeCommands: true });\n try {\n await navigator.clipboard.writeText(text);\n s().toast(s().t?.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');\n } catch (_e) { s().toast(s().t?.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error'); } finally { u({ copyingUnfinishedMergeCommands: false }); }\n },\n\n async handlePull() {\n u({ isPulling: true });\n try {\n await gitService.pull(s().dashboardId, s().envId);\n s().toast(s().t?.git?.pull_success || 'Изменения получены из Git', 'success');\n await this.loadWorkspace();\n } catch (e) {\n const handled = this.openUnfinishedMergeDialogFromError(e);\n if (handled) await this.loadMergeRecoveryState();\n else s().toast(e.message, 'error');\n } finally { u({ isPulling: false }); }\n },\n\n async handlePush() {\n u({ isPushing: true });\n try {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.push_success || 'Изменения отправлены в Git', 'success');\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ isPushing: false }); }\n },\n\n async handlePromote() {\n const st = s();\n if (!st.promoteFromBranch || !st.promoteToBranch || st.promoteFromBranch === st.promoteToBranch) { st.toast('Выберите разные исходную и целевую ветки', 'error'); return; }\n if (st.promoteMode === 'direct' && !String(st.promoteReason || '').trim()) { st.toast('Для небезопасного прямого переноса укажите причину', 'error'); return; }\n u({ promoting: true });\n try {\n const response = await gitService.promote(st.dashboardId, {\n from_branch: st.promoteFromBranch, to_branch: st.promoteToBranch, mode: st.promoteMode,\n title: `Promote ${st.promoteFromBranch} -> ${st.promoteToBranch}: ${st.dashboardTitle || st.dashboardId}`,\n description: st.promoteMode === 'direct' ? `Unsafe direct promote.\\nReason: ${st.promoteReason}` : undefined,\n reason: st.promoteMode === 'direct' ? st.promoteReason : undefined,\n }, st.envId);\n if (st.promoteMode === 'direct') { st.toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning'); }\n else { if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer'); st.toast('Merge Request создан на Git сервере', 'success'); }\n } catch (e) { st.toast(e.message, 'error'); } finally { u({ promoting: false }); }\n },\n\n openDeployModal() {\n const st = s();\n if (st.currentEnvStage === 'PROD') {\n const expected = String(st.dashboardId);\n const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);\n if (String(confirmation || '').trim() !== expected) { st.toast('Подтверждение PROD не пройдено. Деплой отменен.', 'error'); return; }\n }\n u({ showDeployModal: true });\n },\n\n async handleCreateRemoteRepo() {\n const config = resolveDefaultConfig(s().configs, s().selectedConfigId);\n if (!config) { s().toast(s().t?.git?.init_validation_error || 'Сначала выберите Git сервер', 'error'); return; }\n if (!s().selectedConfigId && config.id) u({ selectedConfigId: config.id });\n const suggestedName = buildSuggestedRepoName(s().dashboardTitle, s().dashboardId);\n const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);\n const repoName = String(inputName || '').trim();\n if (!repoName) return;\n u({ creatingRemoteRepo: true });\n try {\n const repo = await gitService.createRemoteRepository(config.id, {\n name: repoName, private: true,\n description: `Superset dashboard ${s().dashboardId}: ${s().dashboardTitle || repoName}`,\n auto_init: true, default_branch: 'main',\n });\n const url = repo?.clone_url || repo?.html_url || '';\n if (!url) throw new Error('Remote repository created, but URL is empty');\n u({ remoteUrl: url });\n s().toast(`Repository created on ${config.provider}`, 'success');\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ creatingRemoteRepo: false }); }\n },\n\n async handleInit() {\n if (!s().selectedConfigId || !s().remoteUrl) { s().toast(s().t?.git?.init_validation_error || 'Заполните все поля', 'error'); return; }\n u({ loading: true });\n try {\n await gitService.initRepository(s().dashboardId, s().selectedConfigId, s().remoteUrl, s().envId);\n s().toast(s().t?.git?.init_success || 'Репозиторий инициализирован', 'success');\n const provider = resolveDefaultConfig(s().configs, s().selectedConfigId)?.provider || '';\n u({ initialized: true, repositoryProvider: provider });\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }\n },\n };\n}\n// #endregion UseGitManager\n" }, { "contract_id": "DocPreview", @@ -78325,15 +84780,28 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TYPE value '{Object}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{Object}" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n{#if previewDoc}\n
\n
\n

{$t.llm.doc_preview_title}

\n \n
\n

{$t.llm.dataset_desc}

\n

{previewDoc.description || 'No description generated.'}

\n\n

{$t.llm.column_doc}

\n \n \n \n \n \n \n \n \n {#each Object.entries(previewDoc.columns || {}) as [name, desc]}\n \n \n \n \n {/each}\n \n
ColumnDescription
{name}{desc}
\n
\n\n
\n \n \n
\n
\n
\n{/if}\n\n\n" + "body": "\n\n\n\n\n\n\n{#if previewDoc}\n
\n
\n

{$t.llm.doc_preview_title}

\n \n
\n

{$t.llm.dataset_desc}

\n

{previewDoc.description || 'No description generated.'}

\n\n

{$t.llm.column_doc}

\n \n \n \n \n \n \n \n \n {#each Object.entries(previewDoc.columns || {}) as [name, desc]}\n \n \n \n \n {/each}\n \n
ColumnDescription
{name}{desc}
\n
\n\n
\n \n \n
\n
\n
\n{/if}\n\n\n" }, { "contract_id": "ProviderConfig", @@ -78383,7 +84851,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n
\n

{$t.llm.providers_title}

\n {\n resetForm();\n showForm = true;\n }}\n >\n {$t.llm.add_provider}\n \n
\n\n {#if showForm}\n \n
\n

\n {editingProvider ? $t.llm.edit_provider : $t.llm.new_provider}\n

\n\n
\n
\n {$t.llm.name}\n \n
\n\n
\n {$t.llm.type}\n updateBaseUrlForType(formData.provider_type)}\n class=\"mt-1 block w-full border rounded-md p-2\"\n >\n \n \n \n \n \n
\n\n
\n {$t.llm.base_url}\n
\n \n \n {isLoadingModels ? \"...\" : \"↻\"}\n \n
\n
\n\n
\n {$t.llm.api_key}\n {#if editingProvider && formData.api_key && isMaskedKey(formData.api_key)}\n
\n \n {formData.api_key}\n \n { formData.api_key = \"\"; }}\n >\n {$t.llm?.change_key || \"Change\"}\n \n
\n

\n {$t.llm?.key_masked_hint || \"Masked for security. Click \\\"Change\\\" to enter a new key.\"}\n

\n {:else}\n \n {/if}\n
\n\n
\n {$t.llm.default_model}\n\n {#if isLoadingModels}\n
\n \n {$t.llm.loading_models}\n
\n {:else if modelsLoadError}\n
{modelsLoadError}
\n {/if}\n\n {#if availableModels.length > 0 && !isLoadingModels}\n \n {#each availableModels as model}\n \n {/each}\n \n
\n {($t.llm.models_loaded || \"{count} models loaded\").replace(\"{count}\", availableModels.length)}\n
\n {:else}\n \n {/if}\n
\n\n
\n \n {$t.llm.active}\n
\n
\n\n {#if testStatus.message}\n \n {testStatus.message}\n
\n {/if}\n\n
\n {\n showForm = false;\n }}\n >\n {$t.llm.cancel}\n \n \n {isTesting ? $t.llm.testing : $t.llm.test}\n \n \n {$t.llm.save}\n \n
\n
\n \n {/if}\n\n
\n {#each providers as provider}\n \n
\n
\n {provider.name}\n \n {provider.is_active ? $t.llm.active : \"Inactive\"}\n \n \n {isMultimodalModel(provider.default_model)\n ? ($t.llm?.multimodal )\n : ($t.llm?.text_only )}\n \n
\n
\n {provider.provider_type} • {provider.default_model}\n
\n
\n
\n handleEdit(provider)}\n >\n {$t.common.edit}\n \n handleDelete(provider)}\n disabled={deletingProviderIds.has(provider.id)}\n >\n {#if deletingProviderIds.has(provider.id)}\n ...\n {:else}\n {$t.common.delete}\n {/if}\n \n toggleActive(provider)}\n disabled={togglingProviderIds.has(provider.id) || deletingProviderIds.has(provider.id)}\n >\n {#if togglingProviderIds.has(provider.id)}\n ...\n {:else}\n {provider.is_active ? \"Deactivate\" : \"Activate\"}\n {/if}\n \n
\n
\n {:else}\n \n {$t.llm.no_providers}\n \n {/each}\n \n\n\n\n" + "body": "\n\n\n\n\n\n\n
\n
\n

{$t.llm.providers_title}

\n {\n resetForm();\n showForm = true;\n }}\n >\n {$t.llm.add_provider}\n \n
\n\n {#if showForm}\n \n
\n

\n {editingProvider ? $t.llm.edit_provider : $t.llm.new_provider}\n

\n\n
\n
\n {$t.llm.name}\n \n
\n\n
\n {$t.llm.type}\n updateBaseUrlForType(formData.provider_type)}\n class=\"mt-1 block w-full border rounded-md p-2\"\n >\n \n \n \n \n \n
\n\n
\n {$t.llm.base_url}\n
\n \n \n {isLoadingModels ? \"...\" : \"↻\"}\n \n
\n
\n\n
\n {$t.llm.api_key}\n {#if editingProvider && formData.api_key && isMaskedKey(formData.api_key)}\n
\n \n {formData.api_key}\n \n { formData.api_key = \"\"; }}\n >\n {$t.llm?.change_key || \"Change\"}\n \n
\n

\n {$t.llm?.key_masked_hint || \"Masked for security. Click \\\"Change\\\" to enter a new key.\"}\n

\n {:else}\n \n {/if}\n
\n\n
\n {$t.llm.default_model}\n\n {#if isLoadingModels}\n
\n \n {$t.llm.loading_models}\n
\n {:else if modelsLoadError}\n
{modelsLoadError}
\n {/if}\n\n {#if availableModels.length > 0 && !isLoadingModels}\n \n {#each availableModels as model}\n \n {/each}\n \n
\n {($t.llm.models_loaded || \"{count} models loaded\").replace(\"{count}\", availableModels.length)}\n
\n {:else}\n \n {/if}\n
\n\n
\n \n {$t.llm.active}\n
\n
\n\n {#if testStatus.message}\n \n {testStatus.message}\n
\n {/if}\n\n
\n {\n showForm = false;\n }}\n >\n {$t.llm.cancel}\n \n \n {isTesting ? $t.llm.testing : $t.llm.test}\n \n \n {$t.llm.save}\n \n
\n
\n \n {/if}\n\n
\n {#each providers as provider}\n \n
\n
\n {provider.name}\n \n {provider.is_active ? $t.llm.active : \"Inactive\"}\n \n \n {isMultimodalModel(provider.default_model)\n ? ($t.llm?.multimodal )\n : ($t.llm?.text_only )}\n \n
\n
\n {provider.provider_type} • {provider.default_model}\n
\n
\n
\n handleEdit(provider)}\n >\n {$t.common.edit}\n \n handleDelete(provider)}\n disabled={deletingProviderIds.has(provider.id)}\n >\n {#if deletingProviderIds.has(provider.id)}\n ...\n {:else}\n {$t.common.delete}\n {/if}\n \n toggleActive(provider)}\n disabled={togglingProviderIds.has(provider.id) || deletingProviderIds.has(provider.id)}\n >\n {#if togglingProviderIds.has(provider.id)}\n ...\n {:else}\n {provider.is_active ? \"Deactivate\" : \"Activate\"}\n {/if}\n \n
\n
\n {:else}\n \n {$t.llm.no_providers}\n \n {/each}\n \n\n\n\n" }, { "contract_id": "ValidationReport", @@ -78765,7 +85233,7 @@ "contract_type": "Component", "file_path": "frontend/src/components/tasks/LogEntryRow.svelte", "start_line": 1, - "end_line": 103, + "end_line": 99, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -78836,10 +85304,23 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TYPE value '{Object} log - The log entry object' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{Object} log - The log entry object" + } }, { "code": "invalid_relation_predicate", @@ -78862,7 +85343,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n \n
\n {formattedTime}\n {log.level || \"INFO\"}\n {#if showSource && log.source}\n {log.source}\n {/if}\n
\n\n \n \n {log.message}\n \n\n \n {#if hasProgress}\n
\n \n
\n \n {progressPercent.toFixed(0)}%\n \n {/if}\n\n\n" + "body": "\n\n\n\n\n\n
\n \n
\n {formattedTime}\n {log.level || \"INFO\"}\n {#if showSource && log.source}\n {log.source}\n {/if}\n
\n\n \n \n {log.message}\n
\n\n \n {#if hasProgress}\n
\n
\n
\n
\n {progressPercent.toFixed(0)}%\n \n {/if}\n\n\n" }, { "contract_id": "formatTime:Function", @@ -78886,7 +85367,7 @@ "contract_type": "Component", "file_path": "frontend/src/components/tasks/LogFilterBar.svelte", "start_line": 1, - "end_line": 143, + "end_line": 144, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -78975,21 +85456,21 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n \n {#each levelOptions as option}\n \n {/each}\n \n\n \n \n {#each availableSources as source}\n \n {/each}\n \n\n
\n \n \n \n \n \n
\n
\n\n {#if hasActiveFilters}\n \n \n \n \n \n {/if}\n\n\n" + "body": "\n\n\n\n\n\n
\n
\n \n \n {#each levelOptions as option}\n \n {/each}\n \n\n \n \n \n {#each availableSources as source}\n \n {/each}\n \n\n \n
\n \n \n \n \n \n
\n
\n\n {#if hasActiveFilters}\n \n \n \n \n \n {/if}\n
\n\n" }, { "contract_id": "TaskLogPanel", "contract_type": "Component", "file_path": "frontend/src/components/tasks/TaskLogPanel.svelte", "start_line": 1, - "end_line": 152, + "end_line": 149, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, "INVARIANT": "Must always display logs in chronological order and respect auto-scroll preference.", "LAYER": "UI", - "PURPOSE": "Combines log filtering and display into a single cohesive dark-themed panel.", + "PURPOSE": "Combines log filtering and display into a single cohesive light-themed panel.", "SEMANTICS": [ "task", "log", @@ -79107,7 +85588,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n \n \n\n \n \n {#if filteredLogs.length === 0}\n \n \n \n \n \n \n \n \n No logs available\n
\n {:else}\n {#each filteredLogs as log}\n \n {/each}\n {/if}\n \n\n \n \n \n {filteredLogs.length}{filteredLogs.length !== logs.length\n ? ` / ${logs.length}`\n : \"\"} entries\n \n \n {#if autoScroll}\n
\n {/if}\n Auto-scroll {autoScroll ? \"on\" : \"off\"}\n \n \n\n\n" + "body": "\n\n\n\n\n\n
\n \n \n\n \n \n {#if filteredLogs.length === 0}\n \n \n \n \n \n \n \n \n No logs available\n
\n {:else}\n {#each filteredLogs as log}\n \n {/each}\n {/if}\n \n\n \n \n \n {filteredLogs.length}{filteredLogs.length !== logs.length\n ? ` / ${logs.length}`\n : \"\"} entries\n \n \n {#if autoScroll}\n
\n {/if}\n Auto-scroll {autoScroll ? \"on\" : \"off\"}\n \n \n\n\n" }, { "contract_id": "TaskResultPanel", @@ -79166,7 +85647,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n{#if !task}\n
\n {$t.tasks?.result_select_task}\n
\n{:else if !result}\n
\n

{$t.tasks?.result_not_structured}

\n
\n{:else if pluginId === 'llm_dashboard_validation'}\n
\n
\n

{$t.tasks?.result_llm_validation}

\n {result.status || 'UNKNOWN'}\n
\n

{result.summary || $t.tasks?.result_no_summary}

\n {#if result.issues?.length}\n
\n

{$t.tasks?.issues} ({result.issues.length})

\n
    \n {#each result.issues as issue}\n
  • \n
    \n {issue.severity}\n {issue.message}\n
    \n {#if issue.location}\n

    {$t.tasks?.location}: {issue.location}

    \n {/if}\n
  • \n {/each}\n
\n
\n {/if}\n
\n{:else if pluginId === 'superset-backup'}\n
\n
\n

{$t.tasks?.backup_result}

\n {result.status || 'UNKNOWN'}\n
\n
\n

{$t.dashboard?.environment}: {result.environment || $t.common?.not_available}

\n

{$t.tasks?.total}: {result.total_dashboards ?? 0}

\n

{$t.tasks?.successful}: {result.backed_up_dashboards ?? 0}

\n

{$t.tasks?.failed}: {result.failed_dashboards ?? 0}

\n
\n {#if result.failures?.length}\n
\n

{$t.tasks?.errors}

\n
    \n {#each result.failures as failure}\n
  • \n {failure.title || failure.id}: {failure.error}\n
  • \n {/each}\n
\n
\n {/if}\n
\n{:else if pluginId === 'superset-migration'}\n
\n
\n

{$t.tasks?.migration_result}

\n {result.status || 'UNKNOWN'}\n
\n
\n

{$t.migration?.source_env}: {result.source_environment || $t.common?.not_available}

\n

{$t.migration?.target_env}: {result.target_environment || $t.common?.not_available}

\n

{$t.tasks?.selected}: {result.selected_dashboards ?? 0}

\n

{$t.tasks?.successful}: {result.migrated_dashboards?.length ?? 0}

\n

{$t.tasks?.with_errors}: {result.failed_dashboards?.length ?? 0}

\n

{$t.tasks?.mappings}: {result.mapping_count ?? 0}

\n
\n {#if result.failed_dashboards?.length}\n
\n

{$t.tasks?.migration_errors}

\n
    \n {#each result.failed_dashboards as failed}\n
  • \n {failed.title || failed.id}: {failed.error}\n
  • \n {/each}\n
\n
\n {/if}\n
\n{:else}\n
\n
{JSON.stringify(result, null, 2)}
\n
\n{/if}\n\n\n" + "body": "\n\n\n\n\n\n\n\n{#if !task}\n
\n {$t.tasks?.result_select_task}\n
\n{:else if !result}\n
\n

{$t.tasks?.result_not_structured}

\n
\n{:else if pluginId === 'llm_dashboard_validation'}\n
\n
\n

{$t.tasks?.result_llm_validation}

\n {result.status || 'UNKNOWN'}\n
\n

{result.summary || $t.tasks?.result_no_summary}

\n {#if result.issues?.length}\n
\n

{$t.tasks?.issues} ({result.issues.length})

\n
    \n {#each result.issues as issue}\n
  • \n
    \n {issue.severity}\n {issue.message}\n
    \n {#if issue.location}\n

    {$t.tasks?.location}: {issue.location}

    \n {/if}\n
  • \n {/each}\n
\n
\n {/if}\n
\n{:else if pluginId === 'superset-backup'}\n
\n
\n

{$t.tasks?.backup_result}

\n {result.status || 'UNKNOWN'}\n
\n
\n

{$t.dashboard?.environment}: {result.environment || $t.common?.not_available}

\n

{$t.tasks?.total}: {result.total_dashboards ?? 0}

\n

{$t.tasks?.successful}: {result.backed_up_dashboards ?? 0}

\n

{$t.tasks?.failed}: {result.failed_dashboards ?? 0}

\n
\n {#if result.failures?.length}\n
\n

{$t.tasks?.errors}

\n
    \n {#each result.failures as failure}\n
  • \n {failure.title || failure.id}: {failure.error}\n
  • \n {/each}\n
\n
\n {/if}\n
\n{:else if pluginId === 'superset-migration'}\n
\n
\n

{$t.tasks?.migration_result}

\n {result.status || 'UNKNOWN'}\n
\n
\n

{$t.migration?.source_env}: {result.source_environment || $t.common?.not_available}

\n

{$t.migration?.target_env}: {result.target_environment || $t.common?.not_available}

\n

{$t.tasks?.selected}: {result.selected_dashboards ?? 0}

\n

{$t.tasks?.successful}: {result.migrated_dashboards?.length ?? 0}

\n

{$t.tasks?.with_errors}: {result.failed_dashboards?.length ?? 0}

\n

{$t.tasks?.mappings}: {result.mapping_count ?? 0}

\n
\n {#if result.failed_dashboards?.length}\n
\n

{$t.tasks?.migration_errors}

\n
    \n {#each result.failed_dashboards as failed}\n
  • \n {failed.title || failed.id}: {failed.error}\n
  • \n {/each}\n
\n
\n {/if}\n
\n{:else}\n
\n
{JSON.stringify(result, null, 2)}
\n
\n{/if}\n\n\n" }, { "contract_id": "ConnectionList", @@ -79270,7 +85751,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n
    \n {#if isLoading}\n
  • {$t.common.loading}
  • \n {:else if connections.length === 0}\n
  • {$t.connections?.no_saved}
  • \n {:else}\n {#each connections as conn}\n
  • \n
    \n
    {conn.name}
    \n
    {conn.type}://{conn.username}@{conn.host}:{conn.port}/{conn.database}
    \n
    \n handleDelete(conn.id)}\n >\n {$t.connections?.delete}\n \n
  • \n {/each}\n {/if}\n
\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n
    \n {#if isLoading}\n
  • {$t.common.loading}
  • \n {:else if connections.length === 0}\n
  • {$t.connections?.no_saved}
  • \n {:else}\n {#each connections as conn}\n
  • \n
    \n
    {conn.name}
    \n
    {conn.type}://{conn.username}@{conn.host}:{conn.port}/{conn.database}
    \n
    \n handleDelete(conn.id)}\n >\n {$t.connections?.delete}\n \n
  • \n {/each}\n {/if}\n
\n
\n\n\n" }, { "contract_id": "fetchConnections:Function", @@ -79535,7 +86016,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n \n
\n
\n
\n ({ value: e.id, label: e.name }))\n ]}\n />\n
\n
\n \n
\n
\n\n
\n {$t.mapper.source}\n
\n \n \n
\n
\n\n {#if source === 'postgres'}\n
\n
\n ({ value: c.id, label: c.name }))\n ]}\n />\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n {:else}\n
\n \n
\n {/if}\n\n
\n \n {#if isGeneratingDocs}\n {$t.mapper?.generating}\n {:else}\n {$t.datasets?.generate_docs}\n {/if}\n \n \n {isRunning ? $t.mapper.starting : $t.mapper.run}\n \n
\n
\n
\n\n generatedDoc = null}\n onSave={handleApplyDoc}\n />\n
\n\n\n" + "body": "\n\n\n\n\n\n\n
\n \n
\n
\n
\n ({ value: e.id, label: e.name }))\n ]}\n />\n
\n
\n \n
\n
\n\n
\n {$t.mapper.source}\n
\n \n \n
\n
\n\n {#if source === 'postgres'}\n
\n
\n ({ value: c.id, label: c.name }))\n ]}\n />\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n {:else}\n
\n \n
\n {/if}\n\n
\n \n {#if isGeneratingDocs}\n {$t.mapper?.generating}\n {:else}\n {$t.datasets?.generate_docs}\n {/if}\n \n \n {isRunning ? $t.mapper.starting : $t.mapper.run}\n \n
\n
\n
\n\n generatedDoc = null}\n onSave={handleApplyDoc}\n />\n
\n\n\n" }, { "contract_id": "fetchData:Function", @@ -79699,12 +86180,13 @@ "contract_type": "Module", "file_path": "frontend/src/lib/api.js", "start_line": 1, - "end_line": 432, + "end_line": 241, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback." + "PURPOSE": "Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback.", + "TYPE": "{any}" }, "relations": [ { @@ -79718,597 +86200,32 @@ "relation_type": "CALLED_BY", "target_id": "TranslateApi", "target_ref": "[TranslateApi]" - }, - { - "source_id": "ApiModule", - "relation_type": "CALLED_BY", - "target_id": "ReportsApi", - "target_ref": "[ReportsApi]" - }, - { - "source_id": "ApiModule", - "relation_type": "CALLED_BY", - "target_id": "DatasetReviewApi", - "target_ref": "[DatasetReviewApi]" - }, - { - "source_id": "ApiModule", - "relation_type": "CALLED_BY", - "target_id": "AssistantApi", - "target_ref": "[AssistantApi]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region ApiModule [C:3] [TYPE Module] [SEMANTICS api, client, fetch, auth, error-handling]\n// @BRIEF Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback.\n// @RELATION DEPENDS_ON -> [ToastsModule]\n// @RELATION CALLED_BY -> [TranslateApi]\n// @RELATION CALLED_BY -> [ReportsApi]\n// @RELATION CALLED_BY -> [DatasetReviewApi]\n// @RELATION CALLED_BY -> [AssistantApi]\n\n// #region api_module:Module [TYPE Function]\n// @SEMANTICS: api, client, fetch, rest\n// @PURPOSE: Handles all communication with the backend API.\n// @LAYER: Infra-API\n// @RELATION: [DEPENDS_ON] ->[toasts_module]\n\nimport { addToast } from './toasts.js';\nimport { PUBLIC_WS_URL } from '$env/static/public';\n\nconst API_BASE_URL = '/api';\n\n// #region buildApiError:Function [TYPE Function]\n// @PURPOSE: Creates a normalized Error object for failed API responses.\n// @PRE: response is a failed fetch Response object.\n// @POST: Returned error contains message and status fields.\nasync function buildApiError(response) {\n const errorData = await response.json().catch(() => ({}));\n const detail = errorData?.detail;\n const message = detail\n ? (\n typeof detail === 'string'\n ? detail\n : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail))\n )\n : `API request failed with status ${response.status}`;\n const error = new Error(message);\n /** @type {any} */ (error).status = response.status;\n /** @type {any} */ (error).detail = detail;\n if (detail && typeof detail === 'object' && detail.error_code) {\n /** @type {any} */ (error).error_code = String(detail.error_code);\n }\n return error;\n}\n// #endregion buildApiError:Function\n\n// #region notifyApiError:Function [TYPE Function]\n// @PURPOSE: Shows toast for API errors with explicit handling of critical statuses.\n// @PRE: error is an Error instance.\n// @POST: User gets visible toast feedback for request failure.\nfunction notifyApiError(error) {\n if (error?.status === 401) {\n addToast(`401 Unauthorized: ${error.message}`, 'error');\n return;\n }\n if (error?.status >= 500) {\n addToast(`Server error (${error.status}): ${error.message}`, 'error');\n return;\n }\n addToast(error.message, 'error');\n}\n// #endregion notifyApiError:Function\n\n// #region shouldSuppressApiErrorToast:Function [TYPE Function]\n// @PURPOSE: Avoid noisy toasts for expected non-critical API failures.\n// @PRE: endpoint can be empty; error can be null.\n// @POST: Returns true only for explicitly allowed suppressed scenarios.\nfunction shouldSuppressApiErrorToast(endpoint, error) {\n const isGitStatusEndpoint =\n typeof endpoint === 'string' &&\n endpoint.startsWith('/git/repositories/') &&\n endpoint.endsWith('/status');\n const isNoRepoError =\n (error?.status === 400 || error?.status === 404) &&\n /Repository for dashboard .* not found/i.test(String(error?.message || ''));\n\n const isGitPullEndpoint =\n typeof endpoint === 'string' &&\n endpoint.startsWith('/git/repositories/') &&\n endpoint.endsWith('/pull');\n const isUnfinishedMergeError =\n error?.status === 409 &&\n (\n String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' ||\n String(error?.detail?.error_code || '') === 'GIT_UNFINISHED_MERGE'\n );\n\n const isDatasetClarificationEndpoint =\n typeof endpoint === 'string' &&\n /\\/dataset-orchestration\\/sessions\\/[^/]+\\/clarification$/.test(endpoint);\n const isMissingClarificationSession =\n error?.status === 404 &&\n /Clarification session not found/i.test(String(error?.message || ''));\n\n return (isGitStatusEndpoint && isNoRepoError)\n || (isGitPullEndpoint && isUnfinishedMergeError)\n || (isDatasetClarificationEndpoint && isMissingClarificationSession);\n}\n// #endregion shouldSuppressApiErrorToast:Function\n\n// #region getWsUrl:Function [TYPE Function]\n// @PURPOSE: Returns the WebSocket URL for a specific task, with fallback logic.\n// @PRE: taskId is provided.\n// @POST: Returns valid WebSocket URL string.\n// @PARAM: taskId (string) - The ID of the task.\n// @RETURN: string - The WebSocket URL.\nexport const getWsUrl = (taskId) => {\n let baseUrl = PUBLIC_WS_URL;\n if (!baseUrl) {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n // Use the current host and port to allow Vite proxy to handle the connection\n baseUrl = `${protocol}//${window.location.host}`;\n }\n return `${baseUrl}/ws/logs/${taskId}`;\n};\n// #endregion getWsUrl:Function\n\n// #region getAuthHeaders:Function [TYPE Function]\n// @PURPOSE: Returns headers with Authorization if token exists.\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = {\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n// #endregion getAuthHeaders:Function\n\n// #region fetchApi:Function [TYPE Function]\n// @PURPOSE: Generic GET request wrapper.\n// @PRE: endpoint string is provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @RETURN: Promise - JSON response.\nasync function fetchApi(endpoint, options = {}) {\n try {\n console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n headers: getAuthHeaders(options.headers || {})\n });\n console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error);\n if (!options.suppressToast) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion fetchApi:Function\n\n// #region fetchApiBlob:Function [TYPE Function]\n// @PURPOSE: Generic GET wrapper for binary payloads.\n// @PRE: endpoint string is provided.\n// @POST: Returns Blob or throws on error.\nasync function fetchApiBlob(endpoint, options = {}) {\n const notifyError = options.notifyError !== false;\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n headers: getAuthHeaders(options.headers || {})\n });\n if (response.status === 202) {\n const payload = await response.json().catch(() => ({ message: \"Resource is being prepared\" }));\n const error = new Error(payload?.message || \"Resource is being prepared\");\n /** @type {any} */ (error).status = 202;\n throw error;\n }\n if (!response.ok) {\n throw await buildApiError(response);\n }\n return await response.blob();\n } catch (error) {\n console.error(`[api.fetchApiBlob][Coherence:Failed] Error fetching blob from ${endpoint}:`, error);\n if (notifyError) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion fetchApiBlob:Function\n\n// #region postApi:Function [TYPE Function]\n// @PURPOSE: Generic POST request wrapper.\n// @PRE: endpoint and body are provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @PARAM: body (object) - Request payload.\n// @RETURN: Promise - JSON response.\nasync function postApi(endpoint, body, options = {}) {\n try {\n console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'POST',\n headers: getAuthHeaders(options.headers || {}),\n body: JSON.stringify(body),\n });\n console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.postApi][Coherence:Failed] Error posting to ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n// #endregion postApi:Function\n\n// #region deleteApi:Function [TYPE Function]\n// @PURPOSE: Generic DELETE request wrapper.\n// @PRE: endpoint is provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @RETURN: Promise - JSON response.\nasync function deleteApi(endpoint, options = {}) {\n try {\n console.log(`[api.deleteApi][Action] Deleting from context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'DELETE',\n headers: getAuthHeaders(options.headers || {}),\n });\n console.log(`[api.deleteApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.deleteApi][Coherence:Failed] Error deleting from ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n// #endregion deleteApi:Function\n\n// #region requestApi:Function [TYPE Function]\n// @PURPOSE: Generic request wrapper.\n// @PRE: endpoint and method are provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\nasync function requestApi(endpoint, method = 'GET', body = null, requestOptions = {}) {\n try {\n console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`);\n const fetchOptions = {\n method,\n headers: getAuthHeaders(requestOptions.headers || {}),\n };\n if (body) {\n fetchOptions.body = JSON.stringify(body);\n }\n const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);\n console.log(`[api.requestApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n const error = await buildApiError(response);\n console.error(`[api.requestApi][Action] Request failed context={{'status': ${response.status}, 'message': '${error.message}'}}`);\n throw error;\n }\n if (response.status === 204) {\n console.log('[api.requestApi][Action] 204 No Content received');\n return null;\n }\n return await response.json();\n } catch (error) {\n console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);\n if (!shouldSuppressApiErrorToast(endpoint, error)) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion requestApi:Function\n\n// #region api:Data [TYPE Function]\n// @PURPOSE: API client object with specific methods.\nexport const api = {\n fetchApi,\n postApi,\n deleteApi,\n requestApi,\n getPlugins: () => fetchApi('/plugins'),\n getTasks: (options = {}) => {\n const params = new URLSearchParams();\n if (options.limit != null) params.append('limit', String(options.limit));\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.status) params.append('status', options.status);\n if (options.task_type) params.append('task_type', options.task_type);\n if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));\n if (Array.isArray(options.plugin_id)) {\n options.plugin_id.forEach((pluginId) => params.append('plugin_id', pluginId));\n }\n const query = params.toString();\n return fetchApi(`/tasks${query ? `?${query}` : ''}`);\n },\n getTask: (taskId) => fetchApi(`/tasks/${taskId}`),\n getTaskLogs: (taskId, options = {}) => {\n const params = new URLSearchParams();\n if (options.level) params.append('level', options.level);\n if (options.source) params.append('source', options.source);\n if (options.search) params.append('search', options.search);\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.limit != null) params.append('limit', String(options.limit));\n const query = params.toString();\n return fetchApi(`/tasks/${taskId}/logs${query ? `?${query}` : ''}`);\n },\n createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),\n\n // Profile\n getProfilePreferences: () => fetchApi('/profile/preferences'),\n updateProfilePreferences: (payload) => requestApi('/profile/preferences', 'PATCH', payload),\n lookupSupersetAccounts: (environmentId, options = {}) => {\n const normalizedEnvironmentId = String(environmentId || '').trim();\n if (!normalizedEnvironmentId) {\n throw new Error('environmentId is required for Superset account lookup');\n }\n const params = new URLSearchParams({ environment_id: normalizedEnvironmentId });\n if (options.search) params.append('search', options.search);\n if (options.page_index != null) params.append('page_index', String(options.page_index));\n if (options.page_size != null) params.append('page_size', String(options.page_size));\n if (options.sort_column) params.append('sort_column', options.sort_column);\n if (options.sort_order) params.append('sort_order', options.sort_order);\n return fetchApi(`/profile/superset-accounts?${params.toString()}`);\n },\n\n // Settings\n getSettings: () => fetchApi('/settings'),\n updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings),\n getEnvironments: () => fetchApi('/settings/environments'),\n addEnvironment: (env) => postApi('/settings/environments', env),\n updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),\n deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),\n testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),\n updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),\n getStorageSettings: () => fetchApi('/settings/storage'),\n updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage),\n getEnvironmentsList: () => fetchApi('/environments'),\n getLlmStatus: () => fetchApi('/llm/status'),\n fetchLlmModels: (payload) => postApi('/llm/providers/fetch-models', payload),\n getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),\n getStorageFileBlob: (path) =>\n fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),\n\n // Dashboards\n getDashboards: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n if (options.page_context) params.append('page_context', options.page_context);\n if (options.apply_profile_default != null) {\n params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));\n }\n if (options.override_show_all != null) {\n params.append('override_show_all', String(Boolean(options.override_show_all)));\n }\n if (options.filters?.title) {\n for (const value of options.filters.title) params.append('filter_title', value);\n }\n if (options.filters?.git_status) {\n for (const value of options.filters.git_status) params.append('filter_git_status', value);\n }\n if (options.filters?.llm_status) {\n for (const value of options.filters.llm_status) params.append('filter_llm_status', value);\n }\n if (options.filters?.changed_on) {\n for (const value of options.filters.changed_on) params.append('filter_changed_on', value);\n }\n if (options.filters?.actor) {\n for (const value of options.filters.actor) params.append('filter_actor', value);\n }\n return fetchApi(`/dashboards?${params.toString()}`);\n },\n getDashboardDetail: (envId, dashboardRef) => fetchApi(`/dashboards/${encodeURIComponent(String(dashboardRef))}?env_id=${envId}`),\n getDashboardTaskHistory: (envId, dashboardRef, options = {}) => {\n const params = new URLSearchParams();\n if (envId) params.append('env_id', envId);\n if (options.limit) params.append('limit', options.limit);\n return fetchApi(`/dashboards/${encodeURIComponent(String(dashboardRef))}/tasks?${params.toString()}`);\n },\n getDashboardThumbnail: (envId, dashboardRef, options = {}) => {\n const params = new URLSearchParams();\n params.append('env_id', envId);\n if (options.force != null) params.append('force', String(Boolean(options.force)));\n return fetchApiBlob(`/dashboards/${encodeURIComponent(String(dashboardRef))}/thumbnail?${params.toString()}`, { notifyError: false });\n },\n getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),\n calculateMigrationDryRun: (payload) => postApi('/migration/dry-run', payload),\n\n // Datasets\n getDatasets: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n return fetchApi(`/datasets?${params.toString()}`);\n },\n getDatasetIds: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n return fetchApi(`/datasets/ids?${params.toString()}`);\n },\n getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),\n\n // Settings\n getConsolidatedSettings: () => fetchApi('/settings/consolidated'),\n updateConsolidatedSettings: (settings) => requestApi('/settings/consolidated', 'PATCH', settings),\n\n // Automation Policies\n getValidationPolicies: () => fetchApi('/settings/automation/policies'),\n createValidationPolicy: (policy) => postApi('/settings/automation/policies', policy),\n updateValidationPolicy: (id, policy) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', policy),\n deleteValidationPolicy: (id) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'),\n getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'),\n\n // Health\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n};\n// #endregion api:Data\n\n// #endregion api_module:Module\n// #endregion ApiModule\n" - }, - { - "contract_id": "api_module:Module", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 9, - "end_line": 431, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "Infra-API", - "PURPOSE": "Handles all communication with the backend API.", - "SEMANTICS": [ - "api", - "client", - "fetch", - "rest" - ] - }, - "relations": [ - { - "source_id": "api_module:Module", - "relation_type": "[DEPENDS_ON]", - "target_id": "toasts_module", - "target_ref": "[toasts_module]" } ], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "LAYER", - "message": "@LAYER is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "invalid_enum_value", - "tag": "LAYER", - "message": "@LAYER value 'Infra-API' is not in allowed enum", + "tag": "TYPE", + "message": "@TYPE value '{any}' is not in allowed enum", "detail": { "allowed": [ - "Core", - "Domain", - "API", - "UI", - "Service", - "Infrastructure", - "Plugin" - ], - "value": "Infra-API" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ "Module", + "Function", + "Class", + "Component", "Block", + "ADR", + "Tombstone", "Skill", "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [DEPENDS_ON] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" ], - "predicate": "[DEPENDS_ON]" + "value": "{any}" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region api_module:Module [TYPE Function]\n// @SEMANTICS: api, client, fetch, rest\n// @PURPOSE: Handles all communication with the backend API.\n// @LAYER: Infra-API\n// @RELATION: [DEPENDS_ON] ->[toasts_module]\n\nimport { addToast } from './toasts.js';\nimport { PUBLIC_WS_URL } from '$env/static/public';\n\nconst API_BASE_URL = '/api';\n\n// #region buildApiError:Function [TYPE Function]\n// @PURPOSE: Creates a normalized Error object for failed API responses.\n// @PRE: response is a failed fetch Response object.\n// @POST: Returned error contains message and status fields.\nasync function buildApiError(response) {\n const errorData = await response.json().catch(() => ({}));\n const detail = errorData?.detail;\n const message = detail\n ? (\n typeof detail === 'string'\n ? detail\n : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail))\n )\n : `API request failed with status ${response.status}`;\n const error = new Error(message);\n /** @type {any} */ (error).status = response.status;\n /** @type {any} */ (error).detail = detail;\n if (detail && typeof detail === 'object' && detail.error_code) {\n /** @type {any} */ (error).error_code = String(detail.error_code);\n }\n return error;\n}\n// #endregion buildApiError:Function\n\n// #region notifyApiError:Function [TYPE Function]\n// @PURPOSE: Shows toast for API errors with explicit handling of critical statuses.\n// @PRE: error is an Error instance.\n// @POST: User gets visible toast feedback for request failure.\nfunction notifyApiError(error) {\n if (error?.status === 401) {\n addToast(`401 Unauthorized: ${error.message}`, 'error');\n return;\n }\n if (error?.status >= 500) {\n addToast(`Server error (${error.status}): ${error.message}`, 'error');\n return;\n }\n addToast(error.message, 'error');\n}\n// #endregion notifyApiError:Function\n\n// #region shouldSuppressApiErrorToast:Function [TYPE Function]\n// @PURPOSE: Avoid noisy toasts for expected non-critical API failures.\n// @PRE: endpoint can be empty; error can be null.\n// @POST: Returns true only for explicitly allowed suppressed scenarios.\nfunction shouldSuppressApiErrorToast(endpoint, error) {\n const isGitStatusEndpoint =\n typeof endpoint === 'string' &&\n endpoint.startsWith('/git/repositories/') &&\n endpoint.endsWith('/status');\n const isNoRepoError =\n (error?.status === 400 || error?.status === 404) &&\n /Repository for dashboard .* not found/i.test(String(error?.message || ''));\n\n const isGitPullEndpoint =\n typeof endpoint === 'string' &&\n endpoint.startsWith('/git/repositories/') &&\n endpoint.endsWith('/pull');\n const isUnfinishedMergeError =\n error?.status === 409 &&\n (\n String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' ||\n String(error?.detail?.error_code || '') === 'GIT_UNFINISHED_MERGE'\n );\n\n const isDatasetClarificationEndpoint =\n typeof endpoint === 'string' &&\n /\\/dataset-orchestration\\/sessions\\/[^/]+\\/clarification$/.test(endpoint);\n const isMissingClarificationSession =\n error?.status === 404 &&\n /Clarification session not found/i.test(String(error?.message || ''));\n\n return (isGitStatusEndpoint && isNoRepoError)\n || (isGitPullEndpoint && isUnfinishedMergeError)\n || (isDatasetClarificationEndpoint && isMissingClarificationSession);\n}\n// #endregion shouldSuppressApiErrorToast:Function\n\n// #region getWsUrl:Function [TYPE Function]\n// @PURPOSE: Returns the WebSocket URL for a specific task, with fallback logic.\n// @PRE: taskId is provided.\n// @POST: Returns valid WebSocket URL string.\n// @PARAM: taskId (string) - The ID of the task.\n// @RETURN: string - The WebSocket URL.\nexport const getWsUrl = (taskId) => {\n let baseUrl = PUBLIC_WS_URL;\n if (!baseUrl) {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n // Use the current host and port to allow Vite proxy to handle the connection\n baseUrl = `${protocol}//${window.location.host}`;\n }\n return `${baseUrl}/ws/logs/${taskId}`;\n};\n// #endregion getWsUrl:Function\n\n// #region getAuthHeaders:Function [TYPE Function]\n// @PURPOSE: Returns headers with Authorization if token exists.\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = {\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n// #endregion getAuthHeaders:Function\n\n// #region fetchApi:Function [TYPE Function]\n// @PURPOSE: Generic GET request wrapper.\n// @PRE: endpoint string is provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @RETURN: Promise - JSON response.\nasync function fetchApi(endpoint, options = {}) {\n try {\n console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n headers: getAuthHeaders(options.headers || {})\n });\n console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error);\n if (!options.suppressToast) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion fetchApi:Function\n\n// #region fetchApiBlob:Function [TYPE Function]\n// @PURPOSE: Generic GET wrapper for binary payloads.\n// @PRE: endpoint string is provided.\n// @POST: Returns Blob or throws on error.\nasync function fetchApiBlob(endpoint, options = {}) {\n const notifyError = options.notifyError !== false;\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n headers: getAuthHeaders(options.headers || {})\n });\n if (response.status === 202) {\n const payload = await response.json().catch(() => ({ message: \"Resource is being prepared\" }));\n const error = new Error(payload?.message || \"Resource is being prepared\");\n /** @type {any} */ (error).status = 202;\n throw error;\n }\n if (!response.ok) {\n throw await buildApiError(response);\n }\n return await response.blob();\n } catch (error) {\n console.error(`[api.fetchApiBlob][Coherence:Failed] Error fetching blob from ${endpoint}:`, error);\n if (notifyError) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion fetchApiBlob:Function\n\n// #region postApi:Function [TYPE Function]\n// @PURPOSE: Generic POST request wrapper.\n// @PRE: endpoint and body are provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @PARAM: body (object) - Request payload.\n// @RETURN: Promise - JSON response.\nasync function postApi(endpoint, body, options = {}) {\n try {\n console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'POST',\n headers: getAuthHeaders(options.headers || {}),\n body: JSON.stringify(body),\n });\n console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.postApi][Coherence:Failed] Error posting to ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n// #endregion postApi:Function\n\n// #region deleteApi:Function [TYPE Function]\n// @PURPOSE: Generic DELETE request wrapper.\n// @PRE: endpoint is provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @RETURN: Promise - JSON response.\nasync function deleteApi(endpoint, options = {}) {\n try {\n console.log(`[api.deleteApi][Action] Deleting from context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'DELETE',\n headers: getAuthHeaders(options.headers || {}),\n });\n console.log(`[api.deleteApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.deleteApi][Coherence:Failed] Error deleting from ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n// #endregion deleteApi:Function\n\n// #region requestApi:Function [TYPE Function]\n// @PURPOSE: Generic request wrapper.\n// @PRE: endpoint and method are provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\nasync function requestApi(endpoint, method = 'GET', body = null, requestOptions = {}) {\n try {\n console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`);\n const fetchOptions = {\n method,\n headers: getAuthHeaders(requestOptions.headers || {}),\n };\n if (body) {\n fetchOptions.body = JSON.stringify(body);\n }\n const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);\n console.log(`[api.requestApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n const error = await buildApiError(response);\n console.error(`[api.requestApi][Action] Request failed context={{'status': ${response.status}, 'message': '${error.message}'}}`);\n throw error;\n }\n if (response.status === 204) {\n console.log('[api.requestApi][Action] 204 No Content received');\n return null;\n }\n return await response.json();\n } catch (error) {\n console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);\n if (!shouldSuppressApiErrorToast(endpoint, error)) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion requestApi:Function\n\n// #region api:Data [TYPE Function]\n// @PURPOSE: API client object with specific methods.\nexport const api = {\n fetchApi,\n postApi,\n deleteApi,\n requestApi,\n getPlugins: () => fetchApi('/plugins'),\n getTasks: (options = {}) => {\n const params = new URLSearchParams();\n if (options.limit != null) params.append('limit', String(options.limit));\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.status) params.append('status', options.status);\n if (options.task_type) params.append('task_type', options.task_type);\n if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));\n if (Array.isArray(options.plugin_id)) {\n options.plugin_id.forEach((pluginId) => params.append('plugin_id', pluginId));\n }\n const query = params.toString();\n return fetchApi(`/tasks${query ? `?${query}` : ''}`);\n },\n getTask: (taskId) => fetchApi(`/tasks/${taskId}`),\n getTaskLogs: (taskId, options = {}) => {\n const params = new URLSearchParams();\n if (options.level) params.append('level', options.level);\n if (options.source) params.append('source', options.source);\n if (options.search) params.append('search', options.search);\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.limit != null) params.append('limit', String(options.limit));\n const query = params.toString();\n return fetchApi(`/tasks/${taskId}/logs${query ? `?${query}` : ''}`);\n },\n createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),\n\n // Profile\n getProfilePreferences: () => fetchApi('/profile/preferences'),\n updateProfilePreferences: (payload) => requestApi('/profile/preferences', 'PATCH', payload),\n lookupSupersetAccounts: (environmentId, options = {}) => {\n const normalizedEnvironmentId = String(environmentId || '').trim();\n if (!normalizedEnvironmentId) {\n throw new Error('environmentId is required for Superset account lookup');\n }\n const params = new URLSearchParams({ environment_id: normalizedEnvironmentId });\n if (options.search) params.append('search', options.search);\n if (options.page_index != null) params.append('page_index', String(options.page_index));\n if (options.page_size != null) params.append('page_size', String(options.page_size));\n if (options.sort_column) params.append('sort_column', options.sort_column);\n if (options.sort_order) params.append('sort_order', options.sort_order);\n return fetchApi(`/profile/superset-accounts?${params.toString()}`);\n },\n\n // Settings\n getSettings: () => fetchApi('/settings'),\n updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings),\n getEnvironments: () => fetchApi('/settings/environments'),\n addEnvironment: (env) => postApi('/settings/environments', env),\n updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),\n deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),\n testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),\n updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),\n getStorageSettings: () => fetchApi('/settings/storage'),\n updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage),\n getEnvironmentsList: () => fetchApi('/environments'),\n getLlmStatus: () => fetchApi('/llm/status'),\n fetchLlmModels: (payload) => postApi('/llm/providers/fetch-models', payload),\n getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),\n getStorageFileBlob: (path) =>\n fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),\n\n // Dashboards\n getDashboards: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n if (options.page_context) params.append('page_context', options.page_context);\n if (options.apply_profile_default != null) {\n params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));\n }\n if (options.override_show_all != null) {\n params.append('override_show_all', String(Boolean(options.override_show_all)));\n }\n if (options.filters?.title) {\n for (const value of options.filters.title) params.append('filter_title', value);\n }\n if (options.filters?.git_status) {\n for (const value of options.filters.git_status) params.append('filter_git_status', value);\n }\n if (options.filters?.llm_status) {\n for (const value of options.filters.llm_status) params.append('filter_llm_status', value);\n }\n if (options.filters?.changed_on) {\n for (const value of options.filters.changed_on) params.append('filter_changed_on', value);\n }\n if (options.filters?.actor) {\n for (const value of options.filters.actor) params.append('filter_actor', value);\n }\n return fetchApi(`/dashboards?${params.toString()}`);\n },\n getDashboardDetail: (envId, dashboardRef) => fetchApi(`/dashboards/${encodeURIComponent(String(dashboardRef))}?env_id=${envId}`),\n getDashboardTaskHistory: (envId, dashboardRef, options = {}) => {\n const params = new URLSearchParams();\n if (envId) params.append('env_id', envId);\n if (options.limit) params.append('limit', options.limit);\n return fetchApi(`/dashboards/${encodeURIComponent(String(dashboardRef))}/tasks?${params.toString()}`);\n },\n getDashboardThumbnail: (envId, dashboardRef, options = {}) => {\n const params = new URLSearchParams();\n params.append('env_id', envId);\n if (options.force != null) params.append('force', String(Boolean(options.force)));\n return fetchApiBlob(`/dashboards/${encodeURIComponent(String(dashboardRef))}/thumbnail?${params.toString()}`, { notifyError: false });\n },\n getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),\n calculateMigrationDryRun: (payload) => postApi('/migration/dry-run', payload),\n\n // Datasets\n getDatasets: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n return fetchApi(`/datasets?${params.toString()}`);\n },\n getDatasetIds: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n return fetchApi(`/datasets/ids?${params.toString()}`);\n },\n getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),\n\n // Settings\n getConsolidatedSettings: () => fetchApi('/settings/consolidated'),\n updateConsolidatedSettings: (settings) => requestApi('/settings/consolidated', 'PATCH', settings),\n\n // Automation Policies\n getValidationPolicies: () => fetchApi('/settings/automation/policies'),\n createValidationPolicy: (policy) => postApi('/settings/automation/policies', policy),\n updateValidationPolicy: (id, policy) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', policy),\n deleteValidationPolicy: (id) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'),\n getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'),\n\n // Health\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n};\n// #endregion api:Data\n\n// #endregion api_module:Module\n" - }, - { - "contract_id": "buildApiError:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 20, - "end_line": 42, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returned error contains message and status fields.", - "PRE": "response is a failed fetch Response object.", - "PURPOSE": "Creates a normalized Error object for failed API responses.", - "TYPE": "{any}" - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region buildApiError:Function [TYPE Function]\n// @PURPOSE: Creates a normalized Error object for failed API responses.\n// @PRE: response is a failed fetch Response object.\n// @POST: Returned error contains message and status fields.\nasync function buildApiError(response) {\n const errorData = await response.json().catch(() => ({}));\n const detail = errorData?.detail;\n const message = detail\n ? (\n typeof detail === 'string'\n ? detail\n : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail))\n )\n : `API request failed with status ${response.status}`;\n const error = new Error(message);\n /** @type {any} */ (error).status = response.status;\n /** @type {any} */ (error).detail = detail;\n if (detail && typeof detail === 'object' && detail.error_code) {\n /** @type {any} */ (error).error_code = String(detail.error_code);\n }\n return error;\n}\n// #endregion buildApiError:Function\n" - }, - { - "contract_id": "notifyApiError:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 44, - "end_line": 59, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "User gets visible toast feedback for request failure.", - "PRE": "error is an Error instance.", - "PURPOSE": "Shows toast for API errors with explicit handling of critical statuses." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region notifyApiError:Function [TYPE Function]\n// @PURPOSE: Shows toast for API errors with explicit handling of critical statuses.\n// @PRE: error is an Error instance.\n// @POST: User gets visible toast feedback for request failure.\nfunction notifyApiError(error) {\n if (error?.status === 401) {\n addToast(`401 Unauthorized: ${error.message}`, 'error');\n return;\n }\n if (error?.status >= 500) {\n addToast(`Server error (${error.status}): ${error.message}`, 'error');\n return;\n }\n addToast(error.message, 'error');\n}\n// #endregion notifyApiError:Function\n" - }, - { - "contract_id": "shouldSuppressApiErrorToast:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 61, - "end_line": 96, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns true only for explicitly allowed suppressed scenarios.", - "PRE": "endpoint can be empty; error can be null.", - "PURPOSE": "Avoid noisy toasts for expected non-critical API failures." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region shouldSuppressApiErrorToast:Function [TYPE Function]\n// @PURPOSE: Avoid noisy toasts for expected non-critical API failures.\n// @PRE: endpoint can be empty; error can be null.\n// @POST: Returns true only for explicitly allowed suppressed scenarios.\nfunction shouldSuppressApiErrorToast(endpoint, error) {\n const isGitStatusEndpoint =\n typeof endpoint === 'string' &&\n endpoint.startsWith('/git/repositories/') &&\n endpoint.endsWith('/status');\n const isNoRepoError =\n (error?.status === 400 || error?.status === 404) &&\n /Repository for dashboard .* not found/i.test(String(error?.message || ''));\n\n const isGitPullEndpoint =\n typeof endpoint === 'string' &&\n endpoint.startsWith('/git/repositories/') &&\n endpoint.endsWith('/pull');\n const isUnfinishedMergeError =\n error?.status === 409 &&\n (\n String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' ||\n String(error?.detail?.error_code || '') === 'GIT_UNFINISHED_MERGE'\n );\n\n const isDatasetClarificationEndpoint =\n typeof endpoint === 'string' &&\n /\\/dataset-orchestration\\/sessions\\/[^/]+\\/clarification$/.test(endpoint);\n const isMissingClarificationSession =\n error?.status === 404 &&\n /Clarification session not found/i.test(String(error?.message || ''));\n\n return (isGitStatusEndpoint && isNoRepoError)\n || (isGitPullEndpoint && isUnfinishedMergeError)\n || (isDatasetClarificationEndpoint && isMissingClarificationSession);\n}\n// #endregion shouldSuppressApiErrorToast:Function\n" - }, - { - "contract_id": "getWsUrl:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 98, - "end_line": 113, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "taskId (string) - The ID of the task.", - "POST": "Returns valid WebSocket URL string.", - "PRE": "taskId is provided.", - "PURPOSE": "Returns the WebSocket URL for a specific task, with fallback logic.", - "RETURN": "string - The WebSocket URL." - }, - "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region getWsUrl:Function [TYPE Function]\n// @PURPOSE: Returns the WebSocket URL for a specific task, with fallback logic.\n// @PRE: taskId is provided.\n// @POST: Returns valid WebSocket URL string.\n// @PARAM: taskId (string) - The ID of the task.\n// @RETURN: string - The WebSocket URL.\nexport const getWsUrl = (taskId) => {\n let baseUrl = PUBLIC_WS_URL;\n if (!baseUrl) {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n // Use the current host and port to allow Vite proxy to handle the connection\n baseUrl = `${protocol}//${window.location.host}`;\n }\n return `${baseUrl}/ws/logs/${taskId}`;\n};\n// #endregion getWsUrl:Function\n" - }, - { - "contract_id": "getAuthHeaders:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 115, - "end_line": 130, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Returns headers with Authorization if token exists." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region getAuthHeaders:Function [TYPE Function]\n// @PURPOSE: Returns headers with Authorization if token exists.\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = {\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n// #endregion getAuthHeaders:Function\n" - }, - { - "contract_id": "fetchApi:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 132, - "end_line": 158, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "endpoint (string) - API endpoint.", - "POST": "Returns Promise resolving to JSON data or throws on error.", - "PRE": "endpoint string is provided.", - "PURPOSE": "Generic GET request wrapper.", - "RETURN": "Promise - JSON response." - }, - "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchApi:Function [TYPE Function]\n// @PURPOSE: Generic GET request wrapper.\n// @PRE: endpoint string is provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @RETURN: Promise - JSON response.\nasync function fetchApi(endpoint, options = {}) {\n try {\n console.log(`[api.fetchApi][Action] Fetching from context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n headers: getAuthHeaders(options.headers || {})\n });\n console.log(`[api.fetchApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.fetchApi][Coherence:Failed] Error fetching from ${endpoint}:`, error);\n if (!options.suppressToast) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion fetchApi:Function\n" - }, - { - "contract_id": "fetchApiBlob:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 160, - "end_line": 188, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns Blob or throws on error.", - "PRE": "endpoint string is provided.", - "PURPOSE": "Generic GET wrapper for binary payloads.", - "TYPE": "{any}" - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchApiBlob:Function [TYPE Function]\n// @PURPOSE: Generic GET wrapper for binary payloads.\n// @PRE: endpoint string is provided.\n// @POST: Returns Blob or throws on error.\nasync function fetchApiBlob(endpoint, options = {}) {\n const notifyError = options.notifyError !== false;\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n headers: getAuthHeaders(options.headers || {})\n });\n if (response.status === 202) {\n const payload = await response.json().catch(() => ({ message: \"Resource is being prepared\" }));\n const error = new Error(payload?.message || \"Resource is being prepared\");\n /** @type {any} */ (error).status = 202;\n throw error;\n }\n if (!response.ok) {\n throw await buildApiError(response);\n }\n return await response.blob();\n } catch (error) {\n console.error(`[api.fetchApiBlob][Coherence:Failed] Error fetching blob from ${endpoint}:`, error);\n if (notifyError) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion fetchApiBlob:Function\n" - }, - { - "contract_id": "postApi:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 190, - "end_line": 217, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "body (object) - Request payload.", - "POST": "Returns Promise resolving to JSON data or throws on error.", - "PRE": "endpoint and body are provided.", - "PURPOSE": "Generic POST request wrapper.", - "RETURN": "Promise - JSON response." - }, - "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region postApi:Function [TYPE Function]\n// @PURPOSE: Generic POST request wrapper.\n// @PRE: endpoint and body are provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @PARAM: body (object) - Request payload.\n// @RETURN: Promise - JSON response.\nasync function postApi(endpoint, body, options = {}) {\n try {\n console.log(`[api.postApi][Action] Posting to context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'POST',\n headers: getAuthHeaders(options.headers || {}),\n body: JSON.stringify(body),\n });\n console.log(`[api.postApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.postApi][Coherence:Failed] Error posting to ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n// #endregion postApi:Function\n" - }, - { - "contract_id": "deleteApi:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 219, - "end_line": 244, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "endpoint (string) - API endpoint.", - "POST": "Returns Promise resolving to JSON data or throws on error.", - "PRE": "endpoint is provided.", - "PURPOSE": "Generic DELETE request wrapper.", - "RETURN": "Promise - JSON response." - }, - "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region deleteApi:Function [TYPE Function]\n// @PURPOSE: Generic DELETE request wrapper.\n// @PRE: endpoint is provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\n// @PARAM: endpoint (string) - API endpoint.\n// @RETURN: Promise - JSON response.\nasync function deleteApi(endpoint, options = {}) {\n try {\n console.log(`[api.deleteApi][Action] Deleting from context={{'endpoint': '${endpoint}'}}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'DELETE',\n headers: getAuthHeaders(options.headers || {}),\n });\n console.log(`[api.deleteApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n throw await buildApiError(response);\n }\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.deleteApi][Coherence:Failed] Error deleting from ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n// #endregion deleteApi:Function\n" - }, - { - "contract_id": "requestApi:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 246, - "end_line": 280, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns Promise resolving to JSON data or throws on error.", - "PRE": "endpoint and method are provided.", - "PURPOSE": "Generic request wrapper." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region requestApi:Function [TYPE Function]\n// @PURPOSE: Generic request wrapper.\n// @PRE: endpoint and method are provided.\n// @POST: Returns Promise resolving to JSON data or throws on error.\nasync function requestApi(endpoint, method = 'GET', body = null, requestOptions = {}) {\n try {\n console.log(`[api.requestApi][Action] ${method} to context={{'endpoint': '${endpoint}'}}`);\n const fetchOptions = {\n method,\n headers: getAuthHeaders(requestOptions.headers || {}),\n };\n if (body) {\n fetchOptions.body = JSON.stringify(body);\n }\n const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);\n console.log(`[api.requestApi][Action] Received response context={{'status': ${response.status}, 'ok': ${response.ok}}}`);\n if (!response.ok) {\n const error = await buildApiError(response);\n console.error(`[api.requestApi][Action] Request failed context={{'status': ${response.status}, 'message': '${error.message}'}}`);\n throw error;\n }\n if (response.status === 204) {\n console.log('[api.requestApi][Action] 204 No Content received');\n return null;\n }\n return await response.json();\n } catch (error) {\n console.error(`[api.requestApi][Coherence:Failed] Error ${method} to ${endpoint}:`, error);\n if (!shouldSuppressApiErrorToast(endpoint, error)) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n// #endregion requestApi:Function\n" - }, - { - "contract_id": "api:Data", - "contract_type": "Function", - "file_path": "frontend/src/lib/api.js", - "start_line": 282, - "end_line": 429, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "API client object with specific methods." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region api:Data [TYPE Function]\n// @PURPOSE: API client object with specific methods.\nexport const api = {\n fetchApi,\n postApi,\n deleteApi,\n requestApi,\n getPlugins: () => fetchApi('/plugins'),\n getTasks: (options = {}) => {\n const params = new URLSearchParams();\n if (options.limit != null) params.append('limit', String(options.limit));\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.status) params.append('status', options.status);\n if (options.task_type) params.append('task_type', options.task_type);\n if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));\n if (Array.isArray(options.plugin_id)) {\n options.plugin_id.forEach((pluginId) => params.append('plugin_id', pluginId));\n }\n const query = params.toString();\n return fetchApi(`/tasks${query ? `?${query}` : ''}`);\n },\n getTask: (taskId) => fetchApi(`/tasks/${taskId}`),\n getTaskLogs: (taskId, options = {}) => {\n const params = new URLSearchParams();\n if (options.level) params.append('level', options.level);\n if (options.source) params.append('source', options.source);\n if (options.search) params.append('search', options.search);\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.limit != null) params.append('limit', String(options.limit));\n const query = params.toString();\n return fetchApi(`/tasks/${taskId}/logs${query ? `?${query}` : ''}`);\n },\n createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),\n\n // Profile\n getProfilePreferences: () => fetchApi('/profile/preferences'),\n updateProfilePreferences: (payload) => requestApi('/profile/preferences', 'PATCH', payload),\n lookupSupersetAccounts: (environmentId, options = {}) => {\n const normalizedEnvironmentId = String(environmentId || '').trim();\n if (!normalizedEnvironmentId) {\n throw new Error('environmentId is required for Superset account lookup');\n }\n const params = new URLSearchParams({ environment_id: normalizedEnvironmentId });\n if (options.search) params.append('search', options.search);\n if (options.page_index != null) params.append('page_index', String(options.page_index));\n if (options.page_size != null) params.append('page_size', String(options.page_size));\n if (options.sort_column) params.append('sort_column', options.sort_column);\n if (options.sort_order) params.append('sort_order', options.sort_order);\n return fetchApi(`/profile/superset-accounts?${params.toString()}`);\n },\n\n // Settings\n getSettings: () => fetchApi('/settings'),\n updateGlobalSettings: (settings) => requestApi('/settings/global', 'PATCH', settings),\n getEnvironments: () => fetchApi('/settings/environments'),\n addEnvironment: (env) => postApi('/settings/environments', env),\n updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),\n deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),\n testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),\n updateEnvironmentSchedule: (id, schedule) => requestApi(`/environments/${id}/schedule`, 'PUT', schedule),\n getStorageSettings: () => fetchApi('/settings/storage'),\n updateStorageSettings: (storage) => requestApi('/settings/storage', 'PUT', storage),\n getEnvironmentsList: () => fetchApi('/environments'),\n getLlmStatus: () => fetchApi('/llm/status'),\n fetchLlmModels: (payload) => postApi('/llm/providers/fetch-models', payload),\n getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),\n getStorageFileBlob: (path) =>\n fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),\n\n // Dashboards\n getDashboards: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n if (options.page_context) params.append('page_context', options.page_context);\n if (options.apply_profile_default != null) {\n params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));\n }\n if (options.override_show_all != null) {\n params.append('override_show_all', String(Boolean(options.override_show_all)));\n }\n if (options.filters?.title) {\n for (const value of options.filters.title) params.append('filter_title', value);\n }\n if (options.filters?.git_status) {\n for (const value of options.filters.git_status) params.append('filter_git_status', value);\n }\n if (options.filters?.llm_status) {\n for (const value of options.filters.llm_status) params.append('filter_llm_status', value);\n }\n if (options.filters?.changed_on) {\n for (const value of options.filters.changed_on) params.append('filter_changed_on', value);\n }\n if (options.filters?.actor) {\n for (const value of options.filters.actor) params.append('filter_actor', value);\n }\n return fetchApi(`/dashboards?${params.toString()}`);\n },\n getDashboardDetail: (envId, dashboardRef) => fetchApi(`/dashboards/${encodeURIComponent(String(dashboardRef))}?env_id=${envId}`),\n getDashboardTaskHistory: (envId, dashboardRef, options = {}) => {\n const params = new URLSearchParams();\n if (envId) params.append('env_id', envId);\n if (options.limit) params.append('limit', options.limit);\n return fetchApi(`/dashboards/${encodeURIComponent(String(dashboardRef))}/tasks?${params.toString()}`);\n },\n getDashboardThumbnail: (envId, dashboardRef, options = {}) => {\n const params = new URLSearchParams();\n params.append('env_id', envId);\n if (options.force != null) params.append('force', String(Boolean(options.force)));\n return fetchApiBlob(`/dashboards/${encodeURIComponent(String(dashboardRef))}/thumbnail?${params.toString()}`, { notifyError: false });\n },\n getDatabaseMappings: (sourceEnvId, targetEnvId) => fetchApi(`/dashboards/db-mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),\n calculateMigrationDryRun: (payload) => postApi('/migration/dry-run', payload),\n\n // Datasets\n getDatasets: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n return fetchApi(`/datasets?${params.toString()}`);\n },\n getDatasetIds: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n return fetchApi(`/datasets/ids?${params.toString()}`);\n },\n getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),\n\n // Settings\n getConsolidatedSettings: () => fetchApi('/settings/consolidated'),\n updateConsolidatedSettings: (settings) => requestApi('/settings/consolidated', 'PATCH', settings),\n\n // Automation Policies\n getValidationPolicies: () => fetchApi('/settings/automation/policies'),\n createValidationPolicy: (policy) => postApi('/settings/automation/policies', policy),\n updateValidationPolicy: (id, policy) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', policy),\n deleteValidationPolicy: (id) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'),\n getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'),\n\n // Health\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n};\n// #endregion api:Data\n" + "body": "// #region ApiModule [C:3] [TYPE Module] [SEMANTICS api, client, fetch, auth, error-handling]\n// @BRIEF Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback.\n// @RELATION DEPENDS_ON -> [ToastsModule]\n// @RELATION CALLED_BY -> [TranslateApi]\nimport { addToast } from './toasts.js';\nimport { PUBLIC_WS_URL } from '$env/static/public';\n\nconst API_BASE_URL = '/api';\n\nasync function buildApiError(response) {\n const errorData = await response.json().catch(() => ({}));\n const detail = errorData?.detail;\n const message = detail\n ? (typeof detail === 'string' ? detail : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail)))\n : `API request failed with status ${response.status}`;\n const error = new Error(message);\n /** @type {any} */ (error).status = response.status;\n /** @type {any} */ (error).detail = detail;\n if (detail && typeof detail === 'object' && detail.error_code) {\n /** @type {any} */ (error).error_code = String(detail.error_code);\n }\n return error;\n}\n\nfunction notifyApiError(error) {\n if (error?.status === 401) { addToast(`401 Unauthorized: ${error.message}`, 'error'); return; }\n if (error?.status >= 500) { addToast(`Server error (${error.status}): ${error.message}`, 'error'); return; }\n addToast(error.message, 'error');\n}\n\nfunction shouldSuppressApiErrorToast(endpoint, error) {\n const isGitStatusEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/status');\n const isNoRepoError = (error?.status === 400 || error?.status === 404) && /Repository for dashboard .* not found/i.test(String(error?.message || ''));\n const isGitPullEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/pull');\n const isUnfinishedMergeError = error?.status === 409 && (String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' || String(error?.detail?.error_code || '') === 'GIT_UNFINISHED_MERGE');\n const isDatasetClarificationEndpoint = typeof endpoint === 'string' && /\\/dataset-orchestration\\/sessions\\/[^/]+\\/clarification$/.test(endpoint);\n const isMissingClarificationSession = error?.status === 404 && /Clarification session not found/i.test(String(error?.message || ''));\n return (isGitStatusEndpoint && isNoRepoError) || (isGitPullEndpoint && isUnfinishedMergeError) || (isDatasetClarificationEndpoint && isMissingClarificationSession);\n}\n\nexport const getWsUrl = (taskId) => {\n let baseUrl = PUBLIC_WS_URL;\n if (!baseUrl) {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n baseUrl = `${protocol}//${window.location.host}`;\n }\n return `${baseUrl}/ws/logs/${taskId}`;\n};\n\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = { 'Content-Type': 'application/json', ...extraHeaders };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) headers['Authorization'] = `Bearer ${token}`;\n }\n return headers;\n}\n\nasync function fetchApi(endpoint, options = {}) {\n try {\n console.log(`[api.fetchApi][Action] Fetching from endpoint=${endpoint}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.fetchApi][Coherence:Failed] ${endpoint}:`, error);\n if (!options.suppressToast) notifyApiError(error);\n throw error;\n }\n}\n\nasync function fetchApiBlob(endpoint, options = {}) {\n const notifyError = options.notifyError !== false;\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });\n if (response.status === 202) {\n const payload = await response.json().catch(() => ({ message: \"Resource is being prepared\" }));\n const error = new Error(payload?.message || \"Resource is being prepared\");\n /** @type {any} */ (error).status = 202;\n throw error;\n }\n if (!response.ok) throw await buildApiError(response);\n return await response.blob();\n } catch (error) {\n if (notifyError) notifyApiError(error);\n throw error;\n }\n}\n\nasync function postApi(endpoint, body, options = {}) {\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'POST', headers: getAuthHeaders(options.headers || {}), body: JSON.stringify(body),\n });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.postApi][Coherence:Failed] ${endpoint}:`, error);\n if (!options.suppressToast) notifyApiError(error);\n throw error;\n }\n}\n\nasync function deleteApi(endpoint, options = {}) {\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'DELETE', headers: getAuthHeaders(options.headers || {}) });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.deleteApi][Coherence:Failed] ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n\nasync function requestApi(endpoint, method = 'GET', body = null, requestOptions = {}) {\n try {\n const fetchOptions = { method, headers: getAuthHeaders(requestOptions.headers || {}) };\n if (body) fetchOptions.body = JSON.stringify(body);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.requestApi][Coherence:Failed] ${method} ${endpoint}:`, error);\n if (!shouldSuppressApiErrorToast(endpoint, error)) notifyApiError(error);\n throw error;\n }\n}\n\nexport const api = {\n fetchApi, postApi, deleteApi, requestApi,\n getPlugins: () => fetchApi('/plugins'),\n getTasks: (options = {}) => {\n const params = new URLSearchParams();\n if (options.limit != null) params.append('limit', String(options.limit));\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.status) params.append('status', options.status);\n if (options.task_type) params.append('task_type', options.task_type);\n if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));\n if (Array.isArray(options.plugin_id)) options.plugin_id.forEach((pid) => params.append('plugin_id', pid));\n const query = params.toString();\n return fetchApi(`/tasks${query ? `?${query}` : ''}`);\n },\n getTask: (taskId) => fetchApi(`/tasks/${taskId}`),\n getTaskLogs: (taskId, options = {}) => {\n const params = new URLSearchParams();\n if (options.level) params.append('level', options.level);\n if (options.source) params.append('source', options.source);\n if (options.search) params.append('search', options.search);\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.limit != null) params.append('limit', String(options.limit));\n return fetchApi(`/tasks/${taskId}/logs${params.toString() ? `?${params.toString()}` : ''}`);\n },\n createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),\n getProfilePreferences: () => fetchApi('/profile/preferences'),\n updateProfilePreferences: (payload) => requestApi('/profile/preferences', 'PATCH', payload),\n lookupSupersetAccounts: (environmentId, options = {}) => {\n const eid = String(environmentId || '').trim();\n if (!eid) throw new Error('environmentId is required for Superset account lookup');\n const params = new URLSearchParams({ environment_id: eid });\n if (options.search) params.append('search', options.search);\n if (options.page_index != null) params.append('page_index', String(options.page_index));\n if (options.page_size != null) params.append('page_size', String(options.page_size));\n if (options.sort_column) params.append('sort_column', options.sort_column);\n if (options.sort_order) params.append('sort_order', options.sort_order);\n return fetchApi(`/profile/superset-accounts?${params.toString()}`);\n },\n getSettings: () => fetchApi('/settings'),\n updateGlobalSettings: (s) => requestApi('/settings/global', 'PATCH', s),\n getEnvironments: () => fetchApi('/settings/environments'),\n addEnvironment: (env) => postApi('/settings/environments', env),\n updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),\n deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),\n testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),\n updateEnvironmentSchedule: (id, s) => requestApi(`/environments/${id}/schedule`, 'PUT', s),\n getStorageSettings: () => fetchApi('/settings/storage'),\n updateStorageSettings: (s) => requestApi('/settings/storage', 'PUT', s),\n getEnvironmentsList: () => fetchApi('/environments'),\n getLlmStatus: () => fetchApi('/llm/status'),\n fetchLlmModels: (p) => postApi('/llm/providers/fetch-models', p),\n getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),\n getStorageFileBlob: (path) => fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),\n getDashboards: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n if (options.page_context) params.append('page_context', options.page_context);\n if (options.apply_profile_default != null) params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));\n if (options.override_show_all != null) params.append('override_show_all', String(Boolean(options.override_show_all)));\n if (options.filters?.title) for (const v of options.filters.title) params.append('filter_title', v);\n if (options.filters?.git_status) for (const v of options.filters.git_status) params.append('filter_git_status', v);\n if (options.filters?.llm_status) for (const v of options.filters.llm_status) params.append('filter_llm_status', v);\n if (options.filters?.changed_on) for (const v of options.filters.changed_on) params.append('filter_changed_on', v);\n if (options.filters?.actor) for (const v of options.filters.actor) params.append('filter_actor', v);\n return fetchApi(`/dashboards?${params.toString()}`);\n },\n getDashboardDetail: (envId, ref) => fetchApi(`/dashboards/${encodeURIComponent(String(ref))}?env_id=${envId}`),\n getDashboardTaskHistory: (envId, ref, opts = {}) => {\n const params = new URLSearchParams();\n if (envId) params.append('env_id', envId);\n if (opts.limit) params.append('limit', opts.limit);\n return fetchApi(`/dashboards/${encodeURIComponent(String(ref))}/tasks?${params.toString()}`);\n },\n getDashboardThumbnail: (envId, ref, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.force != null) params.append('force', String(Boolean(opts.force)));\n return fetchApiBlob(`/dashboards/${encodeURIComponent(String(ref))}/thumbnail?${params.toString()}`, { notifyError: false });\n },\n getDatabaseMappings: (src, tgt) => fetchApi(`/dashboards/db-mappings?source_env_id=${src}&target_env_id=${tgt}`),\n calculateMigrationDryRun: (p) => postApi('/migration/dry-run', p),\n getDatasets: (envId, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.search) params.append('search', opts.search);\n if (opts.page) params.append('page', opts.page);\n if (opts.page_size) params.append('page_size', opts.page_size);\n return fetchApi(`/datasets?${params.toString()}`);\n },\n getDatasetIds: (envId, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.search) params.append('search', opts.search);\n return fetchApi(`/datasets/ids?${params.toString()}`);\n },\n getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),\n getConsolidatedSettings: () => fetchApi('/settings/consolidated'),\n updateConsolidatedSettings: (s) => requestApi('/settings/consolidated', 'PATCH', s),\n getValidationPolicies: () => fetchApi('/settings/automation/policies'),\n createValidationPolicy: (p) => postApi('/settings/automation/policies', p),\n updateValidationPolicy: (id, p) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', p),\n deleteValidationPolicy: (id) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'),\n getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'),\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n};\n// #endregion ApiModule\n" }, { "contract_id": "ReportsApiTest", @@ -81650,1211 +87567,275 @@ "body": "// #region getReportDetail:Function [TYPE Function]\n// @PURPOSE: Fetch one report detail by report_id.\n// @PRE: reportId is non-empty string; valid auth context.\n// @POST: Returns parsed detail payload or structured error object.\nexport async function getReportDetail(reportId) {\n\ttry {\n\t\tconsole.log(`[reports][api][getReportDetail:STARTED] id=${reportId}`);\n\t\tconst res = await api.fetchApi(`/reports/${reportId}`);\n\t\tconsole.log(`[reports][api][getReportDetail:SUCCESS] id=${reportId}`);\n\t\treturn res;\n\t} catch (error) {\n\t\tconsole.error(`[reports][api][getReportDetail:FAILED] id=${reportId}`, error);\n\t\tthrow normalizeApiError(error);\n\t}\n}\n// #endregion getReportDetail:Function\n" }, { - "contract_id": "normalizeTranslateError:Function", - "contract_type": "Function", + "contract_id": "TranslateApi", + "contract_type": "Module", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 15, - "end_line": 27, + "start_line": 1, + "end_line": 28, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "POST": "Returns structured error object with message and code.", - "PRE": "error may be Error/string/object.", - "PURPOSE": "Convert unknown API exceptions into deterministic UI-consumable error objects." + "COMPLEXITY": 2, + "PURPOSE": "Barrel module re-exporting all translate API functions from domain-split modules.", + "RATIONALE": "Decomposed from 664->~30 lines by splitting into domain modules per INV_7.", + "REJECTED": "Keeping all APIs in one file was rejected because it exceeded the 400-line module limit." }, - "relations": [], + "relations": [ + { + "source_id": "TranslateApi", + "relation_type": "DEPENDS_ON", + "target_id": "TranslateJobsApi", + "target_ref": "[TranslateJobsApi]" + }, + { + "source_id": "TranslateApi", + "relation_type": "DEPENDS_ON", + "target_id": "TranslateRunsApi", + "target_ref": "[TranslateRunsApi]" + }, + { + "source_id": "TranslateApi", + "relation_type": "DEPENDS_ON", + "target_id": "TranslateDictionariesApi", + "target_ref": "[TranslateDictionariesApi]" + }, + { + "source_id": "TranslateApi", + "relation_type": "DEPENDS_ON", + "target_id": "TranslateSchedulesApi", + "target_ref": "[TranslateSchedulesApi]" + }, + { + "source_id": "TranslateApi", + "relation_type": "DEPENDS_ON", + "target_id": "TranslateCorrectionsApi", + "target_ref": "[TranslateCorrectionsApi]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region normalizeTranslateError:Function [TYPE Function]\n// @PURPOSE: Convert unknown API exceptions into deterministic UI-consumable error objects.\n// @PRE: error may be Error/string/object.\n// @POST: Returns structured error object with message and code.\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n// #endregion normalizeTranslateError:Function\n" + "body": "// #region TranslateApi [C:2] [TYPE Module] [SEMANTICS translate, api, barrel]\n// @BRIEF Barrel module re-exporting all translate API functions from domain-split modules.\n// @RELATION DEPENDS_ON -> [TranslateJobsApi]\n// @RELATION DEPENDS_ON -> [TranslateRunsApi]\n// @RELATION DEPENDS_ON -> [TranslateDictionariesApi]\n// @RELATION DEPENDS_ON -> [TranslateSchedulesApi]\n// @RELATION DEPENDS_ON -> [TranslateCorrectionsApi]\n// @RATIONALE Decomposed from 664->~30 lines by splitting into domain modules per INV_7.\n// @REJECTED Keeping all APIs in one file was rejected because it exceeded the 400-line module limit.\n\n// Jobs\nexport { fetchJobs, createJob, updateJob, deleteJob, duplicateJob } from './translate/jobs.js';\n\n// Datasources & preview\nexport { fetchDatasourceColumns, fetchDatasources, fetchPreview, approveRow, editRow, rejectRow, acceptPreview, fetchPreviewRecords } from './translate/datasources.js';\n\n// Runs\nexport { triggerRun, fetchRunStatus, fetchRunHistory, fetchRunRecords, retryFailedBatches, retryInsert, cancelRun, fetchRunBatches, fetchAllRuns, fetchRunDetail, fetchJobMetrics, fetchAllMetrics } from './translate/runs.js';\n\n// Dictionaries\nexport { dictionaryApi } from './translate/dictionaries.js';\n\n// Schedules\nexport { fetchSchedule, setSchedule, deleteSchedule, enableSchedule, disableSchedule, fetchNextExecutions } from './translate/schedules.js';\n\n// Corrections & bulk replace\nexport { submitCorrection, submitBulkCorrections, inlineEditCorrection, submitCorrectionToDict, bulkFindReplace, bulkReplacePreview, downloadSkippedCsv } from './translate/corrections.js';\n// #endregion TranslateApi\n" }, { - "contract_id": "fetchJobs:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 29, - "end_line": 45, + "contract_id": "TranslateCorrectionsApi", + "contract_type": "Module", + "file_path": "frontend/src/lib/api/translate/corrections.js", + "start_line": 1, + "end_line": 85, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "POST": "Returns array of job objects.", - "PRE": "Valid auth context.", - "PURPOSE": "Fetch paginated list of translation jobs with optional status filter." + "COMPLEXITY": 2, + "PURPOSE": "API client for translation corrections — inline edits, dictionary submission, bulk find-and-replace, CSV download." }, - "relations": [], + "relations": [ + { + "source_id": "TranslateCorrectionsApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region fetchJobs:Function [TYPE Function]\n// @PURPOSE: Fetch paginated list of translation jobs with optional status filter.\n// @PRE: Valid auth context.\n// @POST: Returns array of job objects.\nexport async function fetchJobs(options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.status) params.append('status', options.status);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/jobs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to load translation jobs');\n\t}\n}\n// #endregion fetchJobs:Function\n" + "body": "// #region TranslateCorrectionsApi [C:2] [TYPE Module] [SEMANTICS translate, corrections, bulk-replace, api]\n// @BRIEF API client for translation corrections — inline edits, dictionary submission, bulk find-and-replace, CSV download.\n// @RELATION DEPENDS_ON -> [ApiModule]\nimport { api } from '$lib/api.js';\n\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n\nexport async function submitCorrection(payload) {\n\ttry {\n\t\treturn await api.postApi('/translate/corrections', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to submit correction');\n\t}\n}\n\nexport async function submitBulkCorrections(payload) {\n\ttry {\n\t\treturn await api.postApi('/translate/corrections/bulk', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to submit bulk corrections');\n\t}\n}\n\nexport async function inlineEditCorrection(runId, recordId, languageCode, data) {\n\ttry {\n\t\treturn await api.requestApi(\n\t\t\t`/translate/runs/${runId}/records/${recordId}/languages/${languageCode}`,\n\t\t\t'PUT',\n\t\t\tdata\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to apply inline correction');\n\t}\n}\n\nexport async function submitCorrectionToDict(runId, recordId, languageCode, dictId) {\n\ttry {\n\t\treturn await api.requestApi(\n\t\t\t`/translate/runs/${runId}/records/${recordId}/languages/${languageCode}/submit-to-dict`,\n\t\t\t'POST',\n\t\t\t{ dictionary_id: dictId }\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to submit correction to dictionary');\n\t}\n}\n\nexport async function bulkFindReplace(runId, data) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/bulk-replace`, data);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to perform bulk find-and-replace');\n\t}\n}\n\nexport async function bulkReplacePreview(runId, data) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/bulk-replace`, { ...data, preview: true });\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to preview bulk find-and-replace');\n\t}\n}\n\nexport async function downloadSkippedCsv(runId) {\n\ttry {\n\t\tconst blob = await api.fetchApiBlob(`/translate/runs/${runId}/skipped.csv`);\n\t\tconst url = URL.createObjectURL(blob);\n\t\tconst a = document.createElement('a');\n\t\ta.href = url;\n\t\ta.download = `skipped-${runId.substring(0, 8)}.csv`;\n\t\tdocument.body.appendChild(a);\n\t\ta.click();\n\t\tdocument.body.removeChild(a);\n\t\tURL.revokeObjectURL(url);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to download skipped CSV');\n\t}\n}\n// #endregion TranslateCorrectionsApi\n" }, { - "contract_id": "createJob:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 47, + "contract_id": "TranslateDatasourcesApi", + "contract_type": "Module", + "file_path": "frontend/src/lib/api/translate/datasources.js", + "start_line": 1, + "end_line": 89, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "API client for translation datasources — column fetching, preview, row-level review actions." + }, + "relations": [ + { + "source_id": "TranslateDatasourcesApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region TranslateDatasourcesApi [C:2] [TYPE Module] [SEMANTICS translate, datasources, columns, preview, api]\n// @BRIEF API client for translation datasources — column fetching, preview, row-level review actions.\n// @RELATION DEPENDS_ON -> [ApiModule]\nimport { api } from '$lib/api.js';\n\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n\nexport async function fetchDatasources(envId, search = '') {\n try {\n return await api.fetchApi(\n `/translate/datasources?env_id=${encodeURIComponent(envId)}&search=${encodeURIComponent(search)}`\n );\n } catch (error) {\n throw normalizeTranslateError(error, 'Failed to fetch datasources');\n }\n}\n\nexport async function fetchDatasourceColumns(datasourceId, envId) {\n\ttry {\n\t\treturn await api.fetchApi(\n\t\t\t`/translate/datasources/${datasourceId}/columns?env_id=${encodeURIComponent(envId)}`\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch datasource columns');\n\t}\n}\n\nexport async function fetchPreview(jobId, sampleSize = 10, envId = '') {\n\ttry {\n\t\tconst body = { sample_size: sampleSize };\n\t\tif (envId) body.env_id = envId;\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview`, body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to run preview');\n\t}\n}\n\nexport async function approveRow(jobId, rowKey, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'approve' };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n\nexport async function editRow(jobId, rowKey, translation, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'edit', translation };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n\nexport async function rejectRow(jobId, rowKey, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'reject' };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n\nexport async function acceptPreview(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to accept preview');\n\t}\n}\n\nexport async function fetchPreviewRecords(sessionId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/preview/${sessionId}/records`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch preview records');\n\t}\n}\n// #endregion TranslateDatasourcesApi\n" + }, + { + "contract_id": "TranslateDictionariesApi", + "contract_type": "Module", + "file_path": "frontend/src/lib/api/translate/dictionaries.js", + "start_line": 1, + "end_line": 109, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "API client for translation dictionary CRUD — list, create, update, delete, entries, import." + }, + "relations": [ + { + "source_id": "TranslateDictionariesApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region TranslateDictionariesApi [C:2] [TYPE Module] [SEMANTICS translate, dictionaries, api, crud, entries]\n// @BRIEF API client for translation dictionary CRUD — list, create, update, delete, entries, import.\n// @RELATION DEPENDS_ON -> [ApiModule]\nimport { api } from '$lib/api.js';\n\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n\nexport const dictionaryApi = {\n\tasync fetchDictionaries(options = {}) {\n\t\ttry {\n\t\t\tconst params = new URLSearchParams();\n\t\t\tif (options.page != null) params.append('page', String(options.page));\n\t\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\t\tconst query = params.toString();\n\t\t\treturn await api.fetchApi(`/translate/dictionaries${query ? `?${query}` : ''}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to load dictionaries');\n\t\t}\n\t},\n\n\tasync createDictionary(payload) {\n\t\ttry {\n\t\t\treturn await api.postApi('/translate/dictionaries', payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to create dictionary');\n\t\t}\n\t},\n\n\tasync getDictionary(dictionaryId) {\n\t\ttry {\n\t\t\treturn await api.fetchApi(`/translate/dictionaries/${dictionaryId}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to load dictionary');\n\t\t}\n\t},\n\n\tasync updateDictionary(dictionaryId, payload) {\n\t\ttry {\n\t\t\treturn await api.requestApi(`/translate/dictionaries/${dictionaryId}`, 'PUT', payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to update dictionary');\n\t\t}\n\t},\n\n\tasync deleteDictionary(dictionaryId) {\n\t\ttry {\n\t\t\treturn await api.deleteApi(`/translate/dictionaries/${dictionaryId}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to delete dictionary');\n\t\t}\n\t},\n\n\tasync fetchEntries(dictionaryId, options = {}) {\n\t\ttry {\n\t\t\tconst params = new URLSearchParams();\n\t\t\tif (options.page != null) params.append('page', String(options.page));\n\t\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\t\tconst query = params.toString();\n\t\t\treturn await api.fetchApi(\n\t\t\t\t`/translate/dictionaries/${dictionaryId}/entries${query ? `?${query}` : ''}`\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to load dictionary entries');\n\t\t}\n\t},\n\n\tasync createEntry(dictionaryId, payload) {\n\t\ttry {\n\t\t\treturn await api.postApi(`/translate/dictionaries/${dictionaryId}/entries`, payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to create dictionary entry');\n\t\t}\n\t},\n\n\tasync updateEntry(dictionaryId, entryId, payload) {\n\t\ttry {\n\t\t\treturn await api.requestApi(\n\t\t\t\t`/translate/dictionaries/${dictionaryId}/entries/${entryId}`,\n\t\t\t\t'PUT',\n\t\t\t\tpayload\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to update dictionary entry');\n\t\t}\n\t},\n\n\tasync deleteEntry(dictionaryId, entryId) {\n\t\ttry {\n\t\t\treturn await api.deleteApi(`/translate/dictionaries/${dictionaryId}/entries/${entryId}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to delete dictionary entry');\n\t\t}\n\t},\n\n\tasync importEntries(dictionaryId, payload) {\n\t\ttry {\n\t\t\treturn await api.postApi(`/translate/dictionaries/${dictionaryId}/import`, payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to import dictionary entries');\n\t\t}\n\t},\n};\n// #endregion TranslateDictionariesApi\n" + }, + { + "contract_id": "TranslateJobsApi", + "contract_type": "Module", + "file_path": "frontend/src/lib/api/translate/jobs.js", + "start_line": 1, "end_line": 58, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "POST": "Returns the created job object.", - "PRE": "payload contains valid job configuration.", - "PURPOSE": "Create a new translation job." + "COMPLEXITY": 2, + "PURPOSE": "API client for translation job CRUD — list, create, update, delete, duplicate." }, - "relations": [], + "relations": [ + { + "source_id": "TranslateJobsApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region createJob:Function [TYPE Function]\n// @PURPOSE: Create a new translation job.\n// @PRE: payload contains valid job configuration.\n// @POST: Returns the created job object.\nexport async function createJob(payload) {\n\ttry {\n\t\treturn await api.postApi('/translate/jobs', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to create translation job');\n\t}\n}\n// #endregion createJob:Function\n" + "body": "// #region TranslateJobsApi [C:2] [TYPE Module] [SEMANTICS translate, jobs, api, crud]\n// @BRIEF API client for translation job CRUD — list, create, update, delete, duplicate.\n// @RELATION DEPENDS_ON -> [ApiModule]\nimport { api } from '$lib/api.js';\n\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n\nexport async function fetchJobs(options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.status) params.append('status', options.status);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/jobs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to load translation jobs');\n\t}\n}\n\nexport async function createJob(payload) {\n\ttry {\n\t\treturn await api.postApi('/translate/jobs', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to create translation job');\n\t}\n}\n\nexport async function updateJob(jobId, payload) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}`, 'PUT', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to update translation job');\n\t}\n}\n\nexport async function deleteJob(jobId) {\n\ttry {\n\t\treturn await api.deleteApi(`/translate/jobs/${jobId}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to delete translation job');\n\t}\n}\n\nexport async function duplicateJob(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/duplicate`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to duplicate translation job');\n\t}\n}\n// #endregion TranslateJobsApi\n" }, { - "contract_id": "updateJob:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 60, - "end_line": 71, + "contract_id": "TranslateRunsApi", + "contract_type": "Module", + "file_path": "frontend/src/lib/api/translate/runs.js", + "start_line": 1, + "end_line": 127, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "POST": "Returns the updated job object.", - "PRE": "jobId is non-empty string; payload contains fields to update.", - "PURPOSE": "Update an existing translation job." + "COMPLEXITY": 2, + "PURPOSE": "API client for translation run control — trigger, status, history, records, retry, cancel, metrics." }, - "relations": [], + "relations": [ + { + "source_id": "TranslateRunsApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region updateJob:Function [TYPE Function]\n// @PURPOSE: Update an existing translation job.\n// @PRE: jobId is non-empty string; payload contains fields to update.\n// @POST: Returns the updated job object.\nexport async function updateJob(jobId, payload) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}`, 'PUT', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to update translation job');\n\t}\n}\n// #endregion updateJob:Function\n" + "body": "// #region TranslateRunsApi [C:2] [TYPE Module] [SEMANTICS translate, runs, api, control, status]\n// @BRIEF API client for translation run control — trigger, status, history, records, retry, cancel, metrics.\n// @RELATION DEPENDS_ON -> [ApiModule]\nimport { api } from '$lib/api.js';\n\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n\nexport async function triggerRun(jobId, full = false) {\n\ttry {\n\t\tconst query = full ? '?full_translation=true' : '';\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/run${query}`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to start translation run');\n\t}\n}\n\nexport async function fetchRunStatus(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run status');\n\t}\n}\n\nexport async function fetchRunHistory(jobId, options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/runs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run history');\n\t}\n}\n\nexport async function fetchRunRecords(runId, options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.status) params.append('status', options.status);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/records${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run records');\n\t}\n}\n\nexport async function retryFailedBatches(runId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/retry`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to retry batches');\n\t}\n}\n\nexport async function retryInsert(runId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/retry-insert`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to retry insert');\n\t}\n}\n\nexport async function cancelRun(runId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/cancel`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to cancel run');\n\t}\n}\n\nexport async function fetchRunBatches(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/batches`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run batches');\n\t}\n}\n\nexport async function fetchAllRuns(options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.job_id) params.append('job_id', options.job_id);\n\t\tif (options.status) params.append('status', options.status);\n\t\tif (options.trigger_type) params.append('trigger_type', options.trigger_type);\n\t\tif (options.created_by) params.append('created_by', options.created_by);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/runs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch runs');\n\t}\n}\n\nexport async function fetchRunDetail(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/detail`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run detail');\n\t}\n}\n\nexport async function fetchJobMetrics(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/metrics`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch job metrics');\n\t}\n}\n\nexport async function fetchAllMetrics() {\n\ttry {\n\t\treturn await api.fetchApi('/translate/metrics');\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch metrics');\n\t}\n}\n// #endregion TranslateRunsApi\n" }, { - "contract_id": "deleteJob:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 73, - "end_line": 84, + "contract_id": "TranslateSchedulesApi", + "contract_type": "Module", + "file_path": "frontend/src/lib/api/translate/schedules.js", + "start_line": 1, + "end_line": 61, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "POST": "Returns null on success.", - "PRE": "jobId is non-empty string.", - "PURPOSE": "Delete a translation job." + "COMPLEXITY": 2, + "PURPOSE": "API client for translation job scheduling — CRUD, enable/disable, next executions." }, - "relations": [], + "relations": [ + { + "source_id": "TranslateSchedulesApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region deleteJob:Function [TYPE Function]\n// @PURPOSE: Delete a translation job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns null on success.\nexport async function deleteJob(jobId) {\n\ttry {\n\t\treturn await api.deleteApi(`/translate/jobs/${jobId}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to delete translation job');\n\t}\n}\n// #endregion deleteJob:Function\n" - }, - { - "contract_id": "duplicateJob:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 86, - "end_line": 97, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns the duplicated job summary.", - "PRE": "jobId is non-empty string.", - "PURPOSE": "Duplicate a translation job." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region duplicateJob:Function [TYPE Function]\n// @PURPOSE: Duplicate a translation job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns the duplicated job summary.\nexport async function duplicateJob(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/duplicate`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to duplicate translation job');\n\t}\n}\n// #endregion duplicateJob:Function\n" - }, - { - "contract_id": "fetchDatasourceColumns:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 99, - "end_line": 208, - "tier": "TIER_1", - "complexity": 1, - "metadata": {}, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchDatasourceColumns:Function [TYPE Function]\n\nexport async function fetchDatasources(envId, search = '') {\n try {\n return await api.fetchApi(\n `/translate/datasources?env_id=${encodeURIComponent(envId)}&search=${encodeURIComponent(search)}`\n );\n } catch (error) {\n throw normalizeTranslateError(error, 'Failed to fetch datasources');\n }\n}\n\nexport async function fetchDatasourceColumns(datasourceId, envId) {\n\ttry {\n\t\treturn await api.fetchApi(\n\t\t\t`/translate/datasources/${datasourceId}/columns?env_id=${encodeURIComponent(envId)}`\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch datasource columns');\n\t}\n}\n\n// #region fetchPreview:Function [TYPE Function]\n// @PURPOSE: Trigger a translation preview for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns preview session with rows and cost estimate.\nexport async function fetchPreview(jobId, sampleSize = 10, envId = '') {\n\ttry {\n\t\tconst body = { sample_size: sampleSize };\n\t\tif (envId) {\n\t\t\tbody.env_id = envId;\n\t\t}\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview`, body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to run preview');\n\t}\n}\n// #endregion fetchPreview:Function\n\n// #region approveRow:Function [TYPE Function]\n// @PURPOSE: Approve a preview row (optionally per language).\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'approve' };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// #endregion approveRow:Function\n\n// #region editRow:Function [TYPE Function]\n// @PURPOSE: Edit a preview row's translation (optionally per language).\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'edit', translation: translation };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// #endregion editRow:Function\n\n// #region rejectRow:Function [TYPE Function]\n// @PURPOSE: Reject a preview row (optionally per language).\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'reject' };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// #endregion rejectRow:Function\n\n// #region acceptPreview:Function [TYPE Function]\n// @PURPOSE: Accept a preview session, enabling full translation execution.\n// @PRE: jobId is non-empty string.\n// @POST: Returns accepted preview session.\nexport async function acceptPreview(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to accept preview');\n\t}\n}\n// #endregion acceptPreview:Function\n\n// #region fetchPreviewRecords:Function [TYPE Function]\n// @PURPOSE: Fetch records for a preview session.\n// @PRE: sessionId is non-empty string.\n// @POST: Returns list of preview records.\nexport async function fetchPreviewRecords(sessionId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/preview/${sessionId}/records`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch preview records');\n\t}\n}\n// #endregion fetchPreviewRecords:Function\n// #endregion fetchDatasourceColumns:Function\n" - }, - { - "contract_id": "fetchPreview:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 121, - "end_line": 136, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns preview session with rows and cost estimate.", - "PRE": "jobId is non-empty string.", - "PURPOSE": "Trigger a translation preview for a job." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchPreview:Function [TYPE Function]\n// @PURPOSE: Trigger a translation preview for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns preview session with rows and cost estimate.\nexport async function fetchPreview(jobId, sampleSize = 10, envId = '') {\n\ttry {\n\t\tconst body = { sample_size: sampleSize };\n\t\tif (envId) {\n\t\t\tbody.env_id = envId;\n\t\t}\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview`, body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to run preview');\n\t}\n}\n// #endregion fetchPreview:Function\n" - }, - { - "contract_id": "approveRow:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 138, - "end_line": 151, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated row.", - "PRE": "jobId and rowKey are non-empty strings.", - "PURPOSE": "Approve a preview row (optionally per language)." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region approveRow:Function [TYPE Function]\n// @PURPOSE: Approve a preview row (optionally per language).\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'approve' };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// #endregion approveRow:Function\n" - }, - { - "contract_id": "editRow:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 153, - "end_line": 166, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated row.", - "PRE": "jobId and rowKey are non-empty strings; translation is non-empty.", - "PURPOSE": "Edit a preview row's translation (optionally per language)." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region editRow:Function [TYPE Function]\n// @PURPOSE: Edit a preview row's translation (optionally per language).\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'edit', translation: translation };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// #endregion editRow:Function\n" - }, - { - "contract_id": "rejectRow:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 168, - "end_line": 181, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated row.", - "PRE": "jobId and rowKey are non-empty strings.", - "PURPOSE": "Reject a preview row (optionally per language)." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region rejectRow:Function [TYPE Function]\n// @PURPOSE: Reject a preview row (optionally per language).\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey, languageCode = null) {\n\ttry {\n\t\tconst body = { action: 'reject' };\n\t\tif (languageCode) body.language_code = languageCode;\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// #endregion rejectRow:Function\n" - }, - { - "contract_id": "acceptPreview:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 183, - "end_line": 194, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns accepted preview session.", - "PRE": "jobId is non-empty string.", - "PURPOSE": "Accept a preview session, enabling full translation execution." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region acceptPreview:Function [TYPE Function]\n// @PURPOSE: Accept a preview session, enabling full translation execution.\n// @PRE: jobId is non-empty string.\n// @POST: Returns accepted preview session.\nexport async function acceptPreview(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to accept preview');\n\t}\n}\n// #endregion acceptPreview:Function\n" - }, - { - "contract_id": "fetchPreviewRecords:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 196, - "end_line": 207, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns list of preview records.", - "PRE": "sessionId is non-empty string.", - "PURPOSE": "Fetch records for a preview session." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchPreviewRecords:Function [TYPE Function]\n// @PURPOSE: Fetch records for a preview session.\n// @PRE: sessionId is non-empty string.\n// @POST: Returns list of preview records.\nexport async function fetchPreviewRecords(sessionId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/preview/${sessionId}/records`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch preview records');\n\t}\n}\n// #endregion fetchPreviewRecords:Function\n" - }, - { - "contract_id": "triggerRun:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 211, - "end_line": 223, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns the created run object.", - "PRE": "jobId is non-empty string.", - "PURPOSE": "Trigger a full translation run for a job." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region triggerRun:Function [TYPE Function]\n// @PURPOSE: Trigger a full translation run for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns the created run object.\nexport async function triggerRun(jobId, full = false) {\n\ttry {\n\t\tconst query = full ? '?full_translation=true' : '';\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/run${query}`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to start translation run');\n\t}\n}\n// #endregion triggerRun:Function\n" - }, - { - "contract_id": "fetchRunStatus:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 225, - "end_line": 236, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns run details with statistics.", - "PRE": "runId is non-empty string.", - "PURPOSE": "Fetch status and statistics for a translation run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchRunStatus:Function [TYPE Function]\n// @PURPOSE: Fetch status and statistics for a translation run.\n// @PRE: runId is non-empty string.\n// @POST: Returns run details with statistics.\nexport async function fetchRunStatus(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run status');\n\t}\n}\n// #endregion fetchRunStatus:Function\n" - }, - { - "contract_id": "fetchRunHistory:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 238, - "end_line": 253, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns paginated list of runs.", - "PRE": "jobId is non-empty string.", - "PURPOSE": "Fetch run history for a job." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchRunHistory:Function [TYPE Function]\n// @PURPOSE: Fetch run history for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns paginated list of runs.\nexport async function fetchRunHistory(jobId, options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/runs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run history');\n\t}\n}\n// #endregion fetchRunHistory:Function\n" - }, - { - "contract_id": "fetchRunRecords:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 255, - "end_line": 271, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns paginated records.", - "PRE": "runId is non-empty string.", - "PURPOSE": "Fetch paginated records for a translation run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchRunRecords:Function [TYPE Function]\n// @PURPOSE: Fetch paginated records for a translation run.\n// @PRE: runId is non-empty string.\n// @POST: Returns paginated records.\nexport async function fetchRunRecords(runId, options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.status) params.append('status', options.status);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/records${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run records');\n\t}\n}\n// #endregion fetchRunRecords:Function\n" - }, - { - "contract_id": "retryFailedBatches:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 273, - "end_line": 284, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated run.", - "PRE": "runId is non-empty string.", - "PURPOSE": "Retry failed batches in a translation run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region retryFailedBatches:Function [TYPE Function]\n// @PURPOSE: Retry failed batches in a translation run.\n// @PRE: runId is non-empty string.\n// @POST: Returns updated run.\nexport async function retryFailedBatches(runId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/retry`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to retry batches');\n\t}\n}\n// #endregion retryFailedBatches:Function\n" - }, - { - "contract_id": "retryInsert:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 286, - "end_line": 297, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated run.", - "PRE": "runId is non-empty string.", - "PURPOSE": "Retry the SQL insert phase for a run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region retryInsert:Function [TYPE Function]\n// @PURPOSE: Retry the SQL insert phase for a run.\n// @PRE: runId is non-empty string.\n// @POST: Returns updated run.\nexport async function retryInsert(runId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/retry-insert`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to retry insert');\n\t}\n}\n// #endregion retryInsert:Function\n" - }, - { - "contract_id": "cancelRun:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 299, - "end_line": 310, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated run.", - "PRE": "runId is non-empty string.", - "PURPOSE": "Cancel a running translation." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region cancelRun:Function [TYPE Function]\n// @PURPOSE: Cancel a running translation.\n// @PRE: runId is non-empty string.\n// @POST: Returns updated run.\nexport async function cancelRun(runId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/cancel`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to cancel run');\n\t}\n}\n// #endregion cancelRun:Function\n" - }, - { - "contract_id": "fetchRunBatches:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 312, - "end_line": 323, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns list of batches.", - "PRE": "runId is non-empty string.", - "PURPOSE": "Fetch batches for a translation run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchRunBatches:Function [TYPE Function]\n// @PURPOSE: Fetch batches for a translation run.\n// @PRE: runId is non-empty string.\n// @POST: Returns list of batches.\nexport async function fetchRunBatches(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/batches`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run batches');\n\t}\n}\n// #endregion fetchRunBatches:Function\n" - }, - { - "contract_id": "dictionaryApi:Variable", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 326, - "end_line": 423, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Namespaced API helpers for terminology dictionary CRUD operations." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region dictionaryApi:Variable [TYPE Function]\n// @PURPOSE: Namespaced API helpers for terminology dictionary CRUD operations.\nexport const dictionaryApi = {\n\tasync fetchDictionaries(options = {}) {\n\t\ttry {\n\t\t\tconst params = new URLSearchParams();\n\t\t\tif (options.page != null) params.append('page', String(options.page));\n\t\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\t\tconst query = params.toString();\n\t\t\treturn await api.fetchApi(`/translate/dictionaries${query ? `?${query}` : ''}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to load dictionaries');\n\t\t}\n\t},\n\n\tasync createDictionary(payload) {\n\t\ttry {\n\t\t\treturn await api.postApi('/translate/dictionaries', payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to create dictionary');\n\t\t}\n\t},\n\n\tasync getDictionary(dictionaryId) {\n\t\ttry {\n\t\t\treturn await api.fetchApi(`/translate/dictionaries/${dictionaryId}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to load dictionary');\n\t\t}\n\t},\n\n\tasync updateDictionary(dictionaryId, payload) {\n\t\ttry {\n\t\t\treturn await api.requestApi(`/translate/dictionaries/${dictionaryId}`, 'PUT', payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to update dictionary');\n\t\t}\n\t},\n\n\tasync deleteDictionary(dictionaryId) {\n\t\ttry {\n\t\t\treturn await api.deleteApi(`/translate/dictionaries/${dictionaryId}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to delete dictionary');\n\t\t}\n\t},\n\n\tasync fetchEntries(dictionaryId, options = {}) {\n\t\ttry {\n\t\t\tconst params = new URLSearchParams();\n\t\t\tif (options.page != null) params.append('page', String(options.page));\n\t\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\t\tconst query = params.toString();\n\t\t\treturn await api.fetchApi(\n\t\t\t\t`/translate/dictionaries/${dictionaryId}/entries${query ? `?${query}` : ''}`\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to load dictionary entries');\n\t\t}\n\t},\n\n\tasync createEntry(dictionaryId, payload) {\n\t\ttry {\n\t\t\treturn await api.postApi(`/translate/dictionaries/${dictionaryId}/entries`, payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to create dictionary entry');\n\t\t}\n\t},\n\n\tasync updateEntry(dictionaryId, entryId, payload) {\n\t\ttry {\n\t\t\treturn await api.requestApi(\n\t\t\t\t`/translate/dictionaries/${dictionaryId}/entries/${entryId}`,\n\t\t\t\t'PUT',\n\t\t\t\tpayload\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to update dictionary entry');\n\t\t}\n\t},\n\n\tasync deleteEntry(dictionaryId, entryId) {\n\t\ttry {\n\t\t\treturn await api.deleteApi(`/translate/dictionaries/${dictionaryId}/entries/${entryId}`);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to delete dictionary entry');\n\t\t}\n\t},\n\n\tasync importEntries(dictionaryId, payload) {\n\t\ttry {\n\t\t\treturn await api.postApi(`/translate/dictionaries/${dictionaryId}/import`, payload);\n\t\t} catch (error) {\n\t\t\tthrow normalizeTranslateError(error, 'Failed to import dictionary entries');\n\t\t}\n\t},\n};\n// #endregion dictionaryApi:Variable\n" - }, - { - "contract_id": "submitCorrection:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 429, - "end_line": 440, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns correction result with conflict info.", - "PRE": "payload contains source_term, incorrect_target_term, corrected_target_term, dictionary_id.", - "PURPOSE": "Submit a single term correction." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region submitCorrection:Function [TYPE Function]\n// @PURPOSE: Submit a single term correction.\n// @PRE: payload contains source_term, incorrect_target_term, corrected_target_term, dictionary_id.\n// @POST: Returns correction result with conflict info.\nexport async function submitCorrection(payload) {\n\ttry {\n\t\treturn await api.postApi('/translate/corrections', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to submit correction');\n\t}\n}\n// #endregion submitCorrection:Function\n" - }, - { - "contract_id": "submitBulkCorrections:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 442, - "end_line": 453, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns bulk result with conflict info.", - "PRE": "payload contains corrections array and dictionary_id.", - "PURPOSE": "Submit multiple term corrections atomically." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region submitBulkCorrections:Function [TYPE Function]\n// @PURPOSE: Submit multiple term corrections atomically.\n// @PRE: payload contains corrections array and dictionary_id.\n// @POST: Returns bulk result with conflict info.\nexport async function submitBulkCorrections(payload) {\n\ttry {\n\t\treturn await api.postApi('/translate/corrections/bulk', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to submit bulk corrections');\n\t}\n}\n// #endregion submitBulkCorrections:Function\n" - }, - { - "contract_id": "fetchSchedule:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 459, - "end_line": 468, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Fetch schedule for a translation job." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchSchedule:Function [TYPE Function]\n// @PURPOSE: Fetch schedule for a translation job.\nexport async function fetchSchedule(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch schedule');\n\t}\n}\n// #endregion fetchSchedule:Function\n" - }, - { - "contract_id": "setSchedule:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 470, - "end_line": 479, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Create or update schedule for a translation job." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region setSchedule:Function [TYPE Function]\n// @PURPOSE: Create or update schedule for a translation job.\nexport async function setSchedule(jobId, payload) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/schedule`, 'PUT', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to set schedule');\n\t}\n}\n// #endregion setSchedule:Function\n" - }, - { - "contract_id": "deleteSchedule:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 481, - "end_line": 490, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Delete schedule for a translation job." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region deleteSchedule:Function [TYPE Function]\n// @PURPOSE: Delete schedule for a translation job.\nexport async function deleteSchedule(jobId) {\n\ttry {\n\t\treturn await api.deleteApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to delete schedule');\n\t}\n}\n// #endregion deleteSchedule:Function\n" - }, - { - "contract_id": "enableSchedule:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 492, - "end_line": 501, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Enable a schedule." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region enableSchedule:Function [TYPE Function]\n// @PURPOSE: Enable a schedule.\nexport async function enableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/enable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to enable schedule');\n\t}\n}\n// #endregion enableSchedule:Function\n" - }, - { - "contract_id": "disableSchedule:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 503, - "end_line": 512, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Disable a schedule." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region disableSchedule:Function [TYPE Function]\n// @PURPOSE: Disable a schedule.\nexport async function disableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/disable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to disable schedule');\n\t}\n}\n// #endregion disableSchedule:Function\n" - }, - { - "contract_id": "fetchNextExecutions:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 514, - "end_line": 523, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Preview next N schedule executions." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchNextExecutions:Function [TYPE Function]\n// @PURPOSE: Preview next N schedule executions.\nexport async function fetchNextExecutions(jobId, n = 3) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule/next-executions?n=${n}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch next executions');\n\t}\n}\n// #endregion fetchNextExecutions:Function\n" - }, - { - "contract_id": "fetchAllRuns:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 529, - "end_line": 546, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Fetch all runs with cross-job filtering and pagination." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchAllRuns:Function [TYPE Function]\n// @PURPOSE: Fetch all runs with cross-job filtering and pagination.\nexport async function fetchAllRuns(options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.job_id) params.append('job_id', options.job_id);\n\t\tif (options.status) params.append('status', options.status);\n\t\tif (options.trigger_type) params.append('trigger_type', options.trigger_type);\n\t\tif (options.created_by) params.append('created_by', options.created_by);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/runs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch runs');\n\t}\n}\n// #endregion fetchAllRuns:Function\n" - }, - { - "contract_id": "fetchRunDetail:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 548, - "end_line": 557, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Fetch detailed run info with config_snapshot, records, events." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchRunDetail:Function [TYPE Function]\n// @PURPOSE: Fetch detailed run info with config_snapshot, records, events.\nexport async function fetchRunDetail(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/detail`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run detail');\n\t}\n}\n// #endregion fetchRunDetail:Function\n" - }, - { - "contract_id": "fetchJobMetrics:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 559, - "end_line": 568, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Fetch aggregated metrics for a specific job." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchJobMetrics:Function [TYPE Function]\n// @PURPOSE: Fetch aggregated metrics for a specific job.\nexport async function fetchJobMetrics(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/metrics`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch job metrics');\n\t}\n}\n// #endregion fetchJobMetrics:Function\n" - }, - { - "contract_id": "fetchAllMetrics:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 570, - "end_line": 579, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Fetch aggregated metrics for all jobs." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region fetchAllMetrics:Function [TYPE Function]\n// @PURPOSE: Fetch aggregated metrics for all jobs.\nexport async function fetchAllMetrics() {\n\ttry {\n\t\treturn await api.fetchApi('/translate/metrics');\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch metrics');\n\t}\n}\n// #endregion fetchAllMetrics:Function\n" - }, - { - "contract_id": "inlineEditCorrection:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 585, - "end_line": 600, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns updated language entry with optional dictionary result.", - "PRE": "runId, recordId, languageCode are non-empty strings; data contains final_value.", - "PURPOSE": "Apply an inline correction to a translated value on a completed run result." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region inlineEditCorrection:Function [TYPE Function]\n// @PURPOSE: Apply an inline correction to a translated value on a completed run result.\n// @PRE: runId, recordId, languageCode are non-empty strings; data contains final_value.\n// @POST: Returns updated language entry with optional dictionary result.\nexport async function inlineEditCorrection(runId, recordId, languageCode, data) {\n\ttry {\n\t\treturn await api.requestApi(\n\t\t\t`/translate/runs/${runId}/records/${recordId}/languages/${languageCode}`,\n\t\t\t'PUT',\n\t\t\tdata\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to apply inline correction');\n\t}\n}\n// #endregion inlineEditCorrection:Function\n" - }, - { - "contract_id": "submitCorrectionToDict:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 602, - "end_line": 617, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns dictionary submission result with conflict info.", - "PRE": "runId, recordId, languageCode are non-empty strings; dictId is provided.", - "PURPOSE": "Submit an inline correction to the dictionary from a run result." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region submitCorrectionToDict:Function [TYPE Function]\n// @PURPOSE: Submit an inline correction to the dictionary from a run result.\n// @PRE: runId, recordId, languageCode are non-empty strings; dictId is provided.\n// @POST: Returns dictionary submission result with conflict info.\nexport async function submitCorrectionToDict(runId, recordId, languageCode, dictId) {\n\ttry {\n\t\treturn await api.requestApi(\n\t\t\t`/translate/runs/${runId}/records/${recordId}/languages/${languageCode}/submit-to-dict`,\n\t\t\t'POST',\n\t\t\t{ dictionary_id: dictId }\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to submit correction to dictionary');\n\t}\n}\n// #endregion submitCorrectionToDict:Function\n" - }, - { - "contract_id": "bulkFindReplace:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 619, - "end_line": 630, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns result with rows_affected, corrections_submitted, and preview list.", - "PRE": "runId is non-empty string; data contains find_pattern, replacement_text, target_language.", - "PURPOSE": "Perform bulk find-and-replace on translated values within a run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region bulkFindReplace:Function [TYPE Function]\n// @PURPOSE: Perform bulk find-and-replace on translated values within a run.\n// @PRE: runId is non-empty string; data contains find_pattern, replacement_text, target_language.\n// @POST: Returns result with rows_affected, corrections_submitted, and preview list.\nexport async function bulkFindReplace(runId, data) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/bulk-replace`, data);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to perform bulk find-and-replace');\n\t}\n}\n// #endregion bulkFindReplace:Function\n" - }, - { - "contract_id": "bulkReplacePreview:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 632, - "end_line": 643, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns preview of affected records (no changes applied).", - "PRE": "runId is non-empty string; data contains find_pattern, replacement_text, target_language.", - "PURPOSE": "Preview bulk find-and-replace on translated values without applying changes." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region bulkReplacePreview:Function [TYPE Function]\n// @PURPOSE: Preview bulk find-and-replace on translated values without applying changes.\n// @PRE: runId is non-empty string; data contains find_pattern, replacement_text, target_language.\n// @POST: Returns preview of affected records (no changes applied).\nexport async function bulkReplacePreview(runId, data) {\n\ttry {\n\t\treturn await api.postApi(`/translate/runs/${runId}/bulk-replace`, { ...data, preview: true });\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to preview bulk find-and-replace');\n\t}\n}\n// #endregion bulkReplacePreview:Function\n" - }, - { - "contract_id": "downloadSkippedCsv:Function", - "contract_type": "Function", - "file_path": "frontend/src/lib/api/translate.js", - "start_line": 645, - "end_line": 663, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Download skipped records CSV for a run." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "// #region downloadSkippedCsv:Function [TYPE Function]\n// @PURPOSE: Download skipped records CSV for a run.\nexport async function downloadSkippedCsv(runId) {\n\ttry {\n\t\tconst blob = await api.fetchApiBlob(`/translate/runs/${runId}/skipped.csv`);\n\t\t// Trigger browser download\n\t\tconst url = URL.createObjectURL(blob);\n\t\tconst a = document.createElement('a');\n\t\ta.href = url;\n\t\ta.download = `skipped-${runId.substring(0, 8)}.csv`;\n\t\tdocument.body.appendChild(a);\n\t\ta.click();\n\t\tdocument.body.removeChild(a);\n\t\tURL.revokeObjectURL(url);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to download skipped CSV');\n\t}\n}\n// #endregion downloadSkippedCsv:Function\n" + "body": "// #region TranslateSchedulesApi [C:2] [TYPE Module] [SEMANTICS translate, schedules, api, cron]\n// @BRIEF API client for translation job scheduling — CRUD, enable/disable, next executions.\n// @RELATION DEPENDS_ON -> [ApiModule]\nimport { api } from '$lib/api.js';\n\nfunction normalizeTranslateError(error, defaultMessage = 'Translation API error') {\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\tdefaultMessage;\n\treturn { message, code: 'TRANSLATE_API_ERROR', retryable: true };\n}\n\nexport async function fetchSchedule(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule`, { suppressToast: true });\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch schedule');\n\t}\n}\n\nexport async function setSchedule(jobId, payload) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/schedule`, 'PUT', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to set schedule');\n\t}\n}\n\nexport async function deleteSchedule(jobId) {\n\ttry {\n\t\treturn await api.deleteApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to delete schedule');\n\t}\n}\n\nexport async function enableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/enable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to enable schedule');\n\t}\n}\n\nexport async function disableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/disable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to disable schedule');\n\t}\n}\n\nexport async function fetchNextExecutions(jobId, n = 3) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule/next-executions?n=${n}`, { suppressToast: true });\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch next executions');\n\t}\n}\n// #endregion TranslateSchedulesApi\n" }, { "contract_id": "PermissionsTest", @@ -83911,7 +88892,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{#if visible && sessionId}\n
\n
\n
\n
{$t.dataset_review?.clarification?.title || \"Clarification queue\"}
\n
{fatigueHint}
\n
\n
\n
\n {$t.dataset_review?.clarification?.progress_label || \"Progress\"}\n
\n
{progressLabel}
\n
{cardState}
\n
\n
\n\n {#if loadingState}\n
\n {$t.dataset_review?.clarification?.messages?.saving || \"Loading clarification state...\"}\n
\n {:else if hasQuestion}\n \n
\n {$t.dataset_review?.clarification?.active_question_label || \"Active question\"}\n
\n
{currentQuestion.question_text}
\n\n
\n
\n {$t.dataset_review?.clarification?.why_it_matters_label || \"Why it matters\"}\n
\n
{currentQuestion.why_it_matters}
\n
\n\n
\n
\n
\n {$t.dataset_review?.clarification?.current_guess_label || \"Current guess\"}\n
\n
\n {currentQuestion.current_guess || $t.dataset_review?.clarification?.current_guess_empty || \"No current guess\"}\n
\n
\n
\n
\n {$t.dataset_review?.clarification?.topic_label || \"Topic\"}\n
\n
{currentQuestion.topic_ref}
\n
\n
\n\n
\n {#each currentQuestion.options || [] as option}\n \n {/each}\n
\n\n \n\n
\n \n \n \n \n
\n \n {:else if clarificationSession}\n
\n
{$t.dataset_review?.clarification?.completed || \"No active clarification question remains.\"}
\n
\n \n
\n
\n {/if}\n\n {#if feedbackQuestionId}\n
\n
\n {$t.dataset_review?.clarification?.feedback_label || \"Feedback\"}\n
\n
\n {feedbackQuestionText || $t.dataset_review?.clarification?.feedback_prompt || \"Record whether the clarification result was useful.\"}\n
\n
\n \n \n
\n
\n {/if}\n\n {#if localMessage || localStatus === \"saving\"}\n
\n {localStatus === \"saving\" ? $t.dataset_review?.clarification?.messages?.saving || \"Saving clarification answer...\" : localMessage}\n
\n {/if}\n
\n{/if}\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{#if visible && sessionId}\n
\n
\n
\n
{$t.dataset_review?.clarification?.title || \"Clarification queue\"}
\n
{fatigueHint}
\n
\n
\n
\n {$t.dataset_review?.clarification?.progress_label || \"Progress\"}\n
\n
{progressLabel}
\n
{cardState}
\n
\n
\n\n {#if loadingState}\n
\n {$t.dataset_review?.clarification?.messages?.saving || \"Loading clarification state...\"}\n
\n {:else if hasQuestion}\n \n
\n {$t.dataset_review?.clarification?.active_question_label || \"Active question\"}\n
\n
{currentQuestion.question_text}
\n\n
\n
\n {$t.dataset_review?.clarification?.why_it_matters_label || \"Why it matters\"}\n
\n
{currentQuestion.why_it_matters}
\n
\n\n
\n
\n
\n {$t.dataset_review?.clarification?.current_guess_label || \"Current guess\"}\n
\n
\n {currentQuestion.current_guess || $t.dataset_review?.clarification?.current_guess_empty || \"No current guess\"}\n
\n
\n
\n
\n {$t.dataset_review?.clarification?.topic_label || \"Topic\"}\n
\n
{currentQuestion.topic_ref}
\n
\n
\n\n
\n {#each currentQuestion.options || [] as option}\n \n {/each}\n
\n\n \n\n
\n \n \n \n \n
\n \n {:else if clarificationSession}\n
\n
{$t.dataset_review?.clarification?.completed || \"No active clarification question remains.\"}
\n
\n \n
\n
\n {/if}\n\n {#if feedbackQuestionId}\n
\n
\n {$t.dataset_review?.clarification?.feedback_label || \"Feedback\"}\n
\n
\n {feedbackQuestionText || $t.dataset_review?.clarification?.feedback_prompt || \"Record whether the clarification result was useful.\"}\n
\n
\n \n \n
\n
\n {/if}\n\n {#if localMessage || localStatus === \"saving\"}\n
\n {localStatus === \"saving\" ? $t.dataset_review?.clarification?.messages?.saving || \"Saving clarification answer...\" : localMessage}\n
\n {/if}\n
\n{/if}\n\n\n" }, { "contract_id": "readJson:Function", @@ -84385,7 +89366,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n \n {$t.dataset_review?.preview?.state_label}: {getStateLabel(effectiveState)}\n \n \n {localStatus === \"saving\"\n ? $t.dataset_review?.preview?.generate_loading\n : $t.dataset_review?.preview?.generate_action}\n \n \n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n
\n
\n\n \n {getPreviewBodyText()}\n \n\n {#if isReadOnlySnapshot}\n
\n Текущее состояние preview рассматривайте как read-only snapshot. Для launch требуется актуальный статус ready.\n
\n {/if}\n\n {#if previewGenerationState === \"debouncing\"}\n
\n Preview regeneration requested. Rapid mapping changes keep launch blocked until the current Superset-side preview finishes and returns ready.\n
\n {/if}\n\n {#if previewRequestedAt}\n
\n Last regeneration request: {new Date(previewRequestedAt).toLocaleTimeString()}\n
\n {/if}\n\n {#if preview}\n
\n \n {$t.dataset_review?.preview?.compiler_label}: {getCompilerLabel()}\n \n \n {$t.dataset_review?.preview?.fingerprint_label}: {preview.preview_fingerprint}\n \n {#if previewTimestamp}\n \n {$t.dataset_review?.preview?.compiled_at_label}: {previewTimestamp}\n \n {/if}\n
\n {/if}\n\n {#if effectiveState === \"ready\" && hasSql}\n
\n
\n
\n {$t.dataset_review?.preview?.sql_block_title}\n
\n
\n {$t.dataset_review?.preview?.compiled_truth_note}\n
\n
\n
{preview.compiled_sql}
\n
\n {:else if effectiveState === \"failed\"}\n {#if previewTechnicalDetails}\n
\n
\n {$t.dataset_review?.preview?.error_details_title || \"Compilation details\"}\n
\n
{previewTechnicalDetails}
\n
\n {/if}\n
\n \n {$t.dataset_review?.preview?.go_to_mapping_action}\n \n
\n {:else if effectiveState === \"stale\"}\n
\n Inputs changed after the last trusted preview. Rapid mapping updates keep this snapshot stale until you explicitly regenerate a fresh Superset preview from this panel.\n
\n
\n \n {$t.dataset_review?.preview?.review_inputs_action}\n \n
\n {/if}\n\n {#if localMessage}\n \n {localMessage}\n \n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n \n {$t.dataset_review?.preview?.state_label}: {getStateLabel(effectiveState)}\n \n \n {localStatus === \"saving\"\n ? $t.dataset_review?.preview?.generate_loading\n : $t.dataset_review?.preview?.generate_action}\n \n \n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n
\n
\n\n \n {getPreviewBodyText()}\n \n\n {#if isReadOnlySnapshot}\n
\n Текущее состояние preview рассматривайте как read-only snapshot. Для launch требуется актуальный статус ready.\n
\n {/if}\n\n {#if previewGenerationState === \"debouncing\"}\n
\n Preview regeneration requested. Rapid mapping changes keep launch blocked until the current Superset-side preview finishes and returns ready.\n
\n {/if}\n\n {#if previewRequestedAt}\n
\n Last regeneration request: {new Date(previewRequestedAt).toLocaleTimeString()}\n
\n {/if}\n\n {#if preview}\n
\n \n {$t.dataset_review?.preview?.compiler_label}: {getCompilerLabel()}\n \n \n {$t.dataset_review?.preview?.fingerprint_label}: {preview.preview_fingerprint}\n \n {#if previewTimestamp}\n \n {$t.dataset_review?.preview?.compiled_at_label}: {previewTimestamp}\n \n {/if}\n
\n {/if}\n\n {#if effectiveState === \"ready\" && hasSql}\n
\n
\n
\n {$t.dataset_review?.preview?.sql_block_title}\n
\n
\n {$t.dataset_review?.preview?.compiled_truth_note}\n
\n
\n
{preview.compiled_sql}
\n
\n {:else if effectiveState === \"failed\"}\n {#if previewTechnicalDetails}\n
\n
\n {$t.dataset_review?.preview?.error_details_title || \"Compilation details\"}\n
\n
{previewTechnicalDetails}
\n
\n {/if}\n
\n \n {$t.dataset_review?.preview?.go_to_mapping_action}\n \n
\n {:else if effectiveState === \"stale\"}\n
\n Inputs changed after the last trusted preview. Rapid mapping updates keep this snapshot stale until you explicitly regenerate a fresh Superset preview from this panel.\n
\n
\n \n {$t.dataset_review?.preview?.review_inputs_action}\n \n
\n {/if}\n\n {#if localMessage}\n \n {localMessage}\n \n {/if}\n
\n\n\n" }, { "contract_id": "ExecutionMappingReview", @@ -84550,7 +89531,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n \n {$t.dataset_review?.mapping?.state_label}: {$t.dataset_review?.mapping?.state?.[reviewState]}\n \n \n {$t.dataset_review?.mapping?.pending_approvals_label}: {pendingApprovalCount}\n \n \n {$t.dataset_review?.mapping?.required_values_label}: {missingRequiredCount}\n \n \n {$t.dataset_review?.mapping?.approve_all_action}\n \n
\n
\n\n {#if sortedMappings.length === 0}\n
\n {$t.dataset_review?.mapping?.empty}\n
\n {:else}\n {#if missingRequiredCount > 0}\n
\n {$t.dataset_review?.mapping?.required_blockers_notice.replace(\n \"{count}\",\n String(missingRequiredCount),\n )}\n
\n {/if}\n\n {#if pendingApprovalCount > 0}\n
\n {$t.dataset_review?.mapping?.approval_notice.replace(\n \"{count}\",\n String(pendingApprovalCount),\n )}\n
\n {/if}\n\n
\n {#each sortedMappings as mapping}\n {@const rowState = getRowState(mapping)}\n {@const templateVariable = rowState.templateVariable}\n {@const importedFilter = rowState.importedFilter}\n {@const isEditing = editingMappingId === mapping.mapping_id}\n {@const isSaving = savingMappingId === mapping.mapping_id}\n {@const rowMessage = rowMessages[mapping.mapping_id] || { status: \"\", text: \"\" }}\n\n \n
\n
\n
\n

\n {importedFilter?.display_name || importedFilter?.filter_name || mapping.mapping_id}\n

\n \n {$t.dataset_review?.mapping?.to_variable_label}: {templateVariable?.variable_name ||\n mapping.variable_id}\n \n {#if templateVariable?.is_required}\n \n {$t.dataset_review?.mapping?.required_badge}\n \n {/if}\n {#if rowState.needsExplicitApproval}\n \n {$t.dataset_review?.mapping?.approval_required_badge}\n \n {/if}\n {#if mapping.approval_state === \"approved\"}\n \n {$t.dataset_review?.mapping?.approved_badge}\n \n {/if}\n
\n\n
\n
\n
\n {$t.dataset_review?.mapping?.source_filter_label}\n
\n
\n {importedFilter?.filter_name || mapping.filter_id}\n
\n
\n \n {$t.dataset_review?.mapping?.source_value_label}:\n {stringifyValue(importedFilter?.raw_value)}\n \n \n {$t.dataset_review?.mapping?.confidence_label}:\n {importedFilter?.confidence_state || ($t.common?.unknown || \"unknown\")}\n \n \n {$t.dataset_review?.mapping?.recovery_label}:\n {importedFilter?.recovery_status || ($t.common?.unknown || \"unknown\")}\n \n
\n
\n\n
\n
\n {$t.dataset_review?.mapping?.effective_value_label}\n
\n
\n {stringifyValue(rowState.effectiveValue)}\n
\n
\n \n {$t.dataset_review?.mapping?.method_label}: {getMethodLabel(mapping.mapping_method)}\n \n \n {$t.dataset_review?.mapping?.approval_label}: {getApprovalLabel(mapping.approval_state)}\n \n {#if mapping.warning_level}\n \n {$t.dataset_review?.mapping?.warning_label}: {getWarningLabel(mapping.warning_level)}\n \n {/if}\n
\n
\n
\n\n {#if mapping.transformation_note}\n
\n \n {$t.dataset_review?.mapping?.transformation_note_label}:\n \n {mapping.transformation_note}\n
\n {/if}\n\n {#if rowState.missingRequiredValue}\n
\n {$t.dataset_review?.mapping?.missing_required_value}\n
\n {/if}\n\n {#if isEditing}\n
\n \n\n \n\n
\n saveManualOverride(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.mapping?.save_override_action}\n \n cancelManualOverride(mapping.mapping_id)}\n disabled={isSaving}\n >\n {$t.common?.cancel}\n \n
\n
\n {/if}\n\n {#if rowMessage.text || rowMessage.status === \"saving\"}\n \n {rowMessage.status === \"saving\"\n ? $t.dataset_review?.mapping?.messages?.saving\n : rowMessage.text}\n
\n {/if}\n
\n\n
\n askAiAboutMapping(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveMappingDraft(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n startManualOverride(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.mapping?.manual_override_action}\n \n\n approveMapping(mapping)}\n disabled={disabled || isSaving || !rowState.needsExplicitApproval}\n >\n {$t.dataset_review?.mapping?.approve_action}\n \n
\n
\n \n {/each}\n \n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n \n {$t.dataset_review?.mapping?.state_label}: {$t.dataset_review?.mapping?.state?.[reviewState]}\n \n \n {$t.dataset_review?.mapping?.pending_approvals_label}: {pendingApprovalCount}\n \n \n {$t.dataset_review?.mapping?.required_values_label}: {missingRequiredCount}\n \n \n {$t.dataset_review?.mapping?.approve_all_action}\n \n
\n
\n\n {#if sortedMappings.length === 0}\n
\n {$t.dataset_review?.mapping?.empty}\n
\n {:else}\n {#if missingRequiredCount > 0}\n
\n {$t.dataset_review?.mapping?.required_blockers_notice.replace(\n \"{count}\",\n String(missingRequiredCount),\n )}\n
\n {/if}\n\n {#if pendingApprovalCount > 0}\n
\n {$t.dataset_review?.mapping?.approval_notice.replace(\n \"{count}\",\n String(pendingApprovalCount),\n )}\n
\n {/if}\n\n
\n {#each sortedMappings as mapping}\n {@const rowState = getRowState(mapping)}\n {@const templateVariable = rowState.templateVariable}\n {@const importedFilter = rowState.importedFilter}\n {@const isEditing = editingMappingId === mapping.mapping_id}\n {@const isSaving = savingMappingId === mapping.mapping_id}\n {@const rowMessage = rowMessages[mapping.mapping_id] || { status: \"\", text: \"\" }}\n\n \n
\n
\n
\n

\n {importedFilter?.display_name || importedFilter?.filter_name || mapping.mapping_id}\n

\n \n {$t.dataset_review?.mapping?.to_variable_label}: {templateVariable?.variable_name ||\n mapping.variable_id}\n \n {#if templateVariable?.is_required}\n \n {$t.dataset_review?.mapping?.required_badge}\n \n {/if}\n {#if rowState.needsExplicitApproval}\n \n {$t.dataset_review?.mapping?.approval_required_badge}\n \n {/if}\n {#if mapping.approval_state === \"approved\"}\n \n {$t.dataset_review?.mapping?.approved_badge}\n \n {/if}\n
\n\n
\n
\n
\n {$t.dataset_review?.mapping?.source_filter_label}\n
\n
\n {importedFilter?.filter_name || mapping.filter_id}\n
\n
\n \n {$t.dataset_review?.mapping?.source_value_label}:\n {stringifyValue(importedFilter?.raw_value)}\n \n \n {$t.dataset_review?.mapping?.confidence_label}:\n {importedFilter?.confidence_state || ($t.common?.unknown || \"unknown\")}\n \n \n {$t.dataset_review?.mapping?.recovery_label}:\n {importedFilter?.recovery_status || ($t.common?.unknown || \"unknown\")}\n \n
\n
\n\n
\n
\n {$t.dataset_review?.mapping?.effective_value_label}\n
\n
\n {stringifyValue(rowState.effectiveValue)}\n
\n
\n \n {$t.dataset_review?.mapping?.method_label}: {getMethodLabel(mapping.mapping_method)}\n \n \n {$t.dataset_review?.mapping?.approval_label}: {getApprovalLabel(mapping.approval_state)}\n \n {#if mapping.warning_level}\n \n {$t.dataset_review?.mapping?.warning_label}: {getWarningLabel(mapping.warning_level)}\n \n {/if}\n
\n
\n
\n\n {#if mapping.transformation_note}\n
\n \n {$t.dataset_review?.mapping?.transformation_note_label}:\n \n {mapping.transformation_note}\n
\n {/if}\n\n {#if rowState.missingRequiredValue}\n
\n {$t.dataset_review?.mapping?.missing_required_value}\n
\n {/if}\n\n {#if isEditing}\n
\n \n\n \n\n
\n saveManualOverride(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.mapping?.save_override_action}\n \n cancelManualOverride(mapping.mapping_id)}\n disabled={isSaving}\n >\n {$t.common?.cancel}\n \n
\n
\n {/if}\n\n {#if rowMessage.text || rowMessage.status === \"saving\"}\n \n {rowMessage.status === \"saving\"\n ? $t.dataset_review?.mapping?.messages?.saving\n : rowMessage.text}\n
\n {/if}\n
\n\n
\n askAiAboutMapping(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveMappingDraft(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n startManualOverride(mapping)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.mapping?.manual_override_action}\n \n\n approveMapping(mapping)}\n disabled={disabled || isSaving || !rowState.needsExplicitApproval}\n >\n {$t.dataset_review?.mapping?.approve_action}\n \n
\n
\n \n {/each}\n \n {/if}\n
\n\n\n" }, { "contract_id": "LaunchConfirmationPanel", @@ -84715,7 +89696,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n \n {$t.dataset_review?.launch?.state_label}: {getStateLabel(panelState)}\n \n
\n\n {#if panelState === \"Blocked\"}\n
\n

\n {$t.dataset_review?.launch?.blocked_title}\n

\n

\n {$t.dataset_review?.launch?.blocked_body}\n

\n\n
\n {#each launchBlockers as blocker}\n
\n
\n {blocker.label}\n
\n {#if blocker.detail}\n
\n \n {blocker.detail}\n
\n
\n {/if}\n jumpTo(blocker.target)}\n >\n {$t.dataset_review?.launch?.resolve_action}\n \n
\n {/each}\n
\n \n {/if}\n\n {#if panelState === \"Ready\"}\n
\n
\n
\n
\n
\n {$t.dataset_review?.launch?.dataset_ref_label}\n
\n
\n {session?.dataset_ref || ($t.common?.unknown || \"unknown\")}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.readiness_label}\n
\n
\n {session?.readiness_state || ($t.common?.unknown || \"unknown\")}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.approved_mappings_label}\n
\n
\n {approvedMappingsCount}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.preview_fingerprint_label}\n
\n
\n {preview?.preview_fingerprint || ($t.common?.not_available || \"N/A\")}\n
\n
\n
\n
\n\n
\n

\n {$t.dataset_review?.launch?.sql_lab_target_title}\n

\n

\n {$t.dataset_review?.launch?.sql_lab_target_body}\n

\n
\n \n {$t.dataset_review?.launch?.preview_status_label}: {preview?.preview_status}\n \n \n {$t.dataset_review?.launch?.compiled_by_label}: {preview?.compiled_by}\n \n
\n
\n\n \n {launchButtonLabel}\n \n
\n {/if}\n\n {#if panelState === \"Submitted\"}\n
\n
\n

\n {$t.dataset_review?.launch?.submitted_title}\n

\n

\n {$t.dataset_review?.launch?.submitted_body}\n

\n
\n\n
\n
\n
\n {$t.dataset_review?.launch?.run_context_label}\n
\n
\n {activeRunContext?.run_context_id}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.sql_lab_session_label}\n
\n
\n {activeRunContext?.sql_lab_session_ref}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.launch_status_label}\n
\n
\n {activeRunContext?.launch_status}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.preview_ref_label}\n
\n
\n {activeRunContext?.preview_id}\n
\n
\n
\n
\n {/if}\n\n {#if launchMessage}\n \n {launchMessage}\n \n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n \n {$t.dataset_review?.launch?.state_label}: {getStateLabel(panelState)}\n \n
\n\n {#if panelState === \"Blocked\"}\n
\n

\n {$t.dataset_review?.launch?.blocked_title}\n

\n

\n {$t.dataset_review?.launch?.blocked_body}\n

\n\n
\n {#each launchBlockers as blocker}\n
\n
\n {blocker.label}\n
\n {#if blocker.detail}\n
\n \n {blocker.detail}\n
\n
\n {/if}\n jumpTo(blocker.target)}\n >\n {$t.dataset_review?.launch?.resolve_action}\n \n
\n {/each}\n
\n \n {/if}\n\n {#if panelState === \"Ready\"}\n
\n
\n
\n
\n
\n {$t.dataset_review?.launch?.dataset_ref_label}\n
\n
\n {session?.dataset_ref || ($t.common?.unknown || \"unknown\")}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.readiness_label}\n
\n
\n {session?.readiness_state || ($t.common?.unknown || \"unknown\")}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.approved_mappings_label}\n
\n
\n {approvedMappingsCount}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.preview_fingerprint_label}\n
\n
\n {preview?.preview_fingerprint || ($t.common?.not_available || \"N/A\")}\n
\n
\n
\n
\n\n
\n

\n {$t.dataset_review?.launch?.sql_lab_target_title}\n

\n

\n {$t.dataset_review?.launch?.sql_lab_target_body}\n

\n
\n \n {$t.dataset_review?.launch?.preview_status_label}: {preview?.preview_status}\n \n \n {$t.dataset_review?.launch?.compiled_by_label}: {preview?.compiled_by}\n \n
\n
\n\n \n {launchButtonLabel}\n \n
\n {/if}\n\n {#if panelState === \"Submitted\"}\n
\n
\n

\n {$t.dataset_review?.launch?.submitted_title}\n

\n

\n {$t.dataset_review?.launch?.submitted_body}\n

\n
\n\n
\n
\n
\n {$t.dataset_review?.launch?.run_context_label}\n
\n
\n {activeRunContext?.run_context_id}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.sql_lab_session_label}\n
\n
\n {activeRunContext?.sql_lab_session_ref}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.launch_status_label}\n
\n
\n {activeRunContext?.launch_status}\n
\n
\n\n
\n
\n {$t.dataset_review?.launch?.preview_ref_label}\n
\n
\n {activeRunContext?.preview_id}\n
\n
\n
\n
\n {/if}\n\n {#if launchMessage}\n \n {launchMessage}\n \n {/if}\n
\n\n\n" }, { "contract_id": "SemanticLayerReview", @@ -84880,7 +89861,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n
\n
\n {$t.dataset_review?.semantics?.field_count_label}\n
\n
{activeFieldCount}
\n
\n
\n
Pending review
\n
{reviewPendingCount}
\n
\n \n {$t.dataset_review?.semantics?.approve_all_action}\n \n
\n
\n\n {#if activeFieldCount === 0}\n
\n {$t.dataset_review?.semantics?.empty}\n
\n {:else}\n {#if reviewPendingCount > 0}\n
\n Есть поля, требующие решения. Это не блокирует обзор, но может повлиять на итоговую готовность к launch.\n
\n {/if}\n\n
\n {#each sortedFields as field}\n {@const message = fieldMessages[field.field_id] || { status: \"\", text: \"\" }}\n {@const isEditing = editingFieldId === field.field_id}\n {@const isSaving = savingFieldId === field.field_id}\n\n \n
\n
\n
\n

{field.field_name}

\n \n {field.field_kind}\n \n \n {field.is_locked\n ? $t.dataset_review?.semantics?.locked_badge\n : $t.dataset_review?.semantics?.unlocked_badge}\n \n {#if field.has_conflict}\n \n {$t.dataset_review?.semantics?.conflict_badge}\n \n {/if}\n {#if field.needs_review}\n \n {$t.dataset_review?.semantics?.needs_review_badge}\n \n {/if}\n
\n\n
\n
\n {$t.dataset_review?.semantics?.active_value_label}\n
\n
\n {getCurrentValueSummary(field)}\n
\n
\n \n {$t.dataset_review?.semantics?.provenance_label}:\n {getProvenanceLabel(field.provenance)}\n \n \n {$t.dataset_review?.semantics?.confidence_label}:\n {getConfidenceLabel(field.confidence_rank)}\n \n {#if field.source_id}\n \n {$t.dataset_review?.semantics?.source_label}: {getSourceLabel(field.source_id)}\n \n {/if}\n \n {$t.dataset_review?.semantics?.changed_by_label}: {field.last_changed_by}\n \n
\n
\n\n {#if isEditing}\n
\n \n\n \n\n \n\n
\n saveManualOverride(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.save_manual_action}\n \n cancelManualEdit(field.field_id)}\n disabled={isSaving}\n >\n {$t.common?.cancel}\n \n
\n
\n {/if}\n\n
\n
\n

\n {$t.dataset_review?.semantics?.candidates_title}\n

\n \n {field.candidates?.length || 0}\n \n
\n\n {#if !field.candidates?.length}\n

\n {$t.dataset_review?.semantics?.candidates_empty}\n

\n {:else}\n
\n {#each field.candidates as candidate}\n
\n
\n
\n
\n
\n {candidate.proposed_verbose_name ||\n $t.dataset_review?.semantics?.empty_value}\n
\n \n {candidate.match_type}\n \n \n {$t.dataset_review?.semantics?.score_label}:\n {candidate.confidence_score}\n \n \n {getCandidateStatusLabel(candidate.status)}\n \n
\n\n

\n {candidate.proposed_description ||\n $t.dataset_review?.semantics?.candidate_description_empty}\n

\n\n {#if candidate.proposed_display_format}\n

\n {$t.dataset_review?.semantics?.display_format_label}:\n {candidate.proposed_display_format}\n

\n {/if}\n
\n\n
\n acceptCandidate(field, candidate.candidate_id, false)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.apply_candidate_action}\n \n acceptCandidate(field, candidate.candidate_id, true)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.apply_and_lock_action}\n \n
\n
\n
\n {/each}\n
\n {/if}\n
\n\n {#if message.text || message.status === \"saving\"}\n \n {message.status === \"saving\"\n ? $t.dataset_review?.semantics?.messages?.saving\n : message.text}\n
\n {/if}\n
\n\n
\n askAiAboutField(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFieldDraft(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n startManualEdit(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.manual_override_action}\n \n\n mutateLock(field, field.is_locked ? \"unlock\" : \"lock\")}\n disabled={disabled || isSaving}\n >\n {field.is_locked\n ? $t.dataset_review?.semantics?.unlock_action\n : $t.dataset_review?.semantics?.lock_action}\n \n\n
\n recordFeedback(field.field_id, 'up')}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.feedback_up_action}\n \n recordFeedback(field.field_id, 'down')}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.feedback_down_action}\n \n
\n
\n
\n \n {/each}\n \n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n
\n
\n {$t.dataset_review?.semantics?.field_count_label}\n
\n
{activeFieldCount}
\n
\n
\n
Pending review
\n
{reviewPendingCount}
\n
\n \n {$t.dataset_review?.semantics?.approve_all_action}\n \n
\n
\n\n {#if activeFieldCount === 0}\n
\n {$t.dataset_review?.semantics?.empty}\n
\n {:else}\n {#if reviewPendingCount > 0}\n
\n Есть поля, требующие решения. Это не блокирует обзор, но может повлиять на итоговую готовность к launch.\n
\n {/if}\n\n
\n {#each sortedFields as field}\n {@const message = fieldMessages[field.field_id] || { status: \"\", text: \"\" }}\n {@const isEditing = editingFieldId === field.field_id}\n {@const isSaving = savingFieldId === field.field_id}\n\n \n
\n
\n
\n

{field.field_name}

\n \n {field.field_kind}\n \n \n {field.is_locked\n ? $t.dataset_review?.semantics?.locked_badge\n : $t.dataset_review?.semantics?.unlocked_badge}\n \n {#if field.has_conflict}\n \n {$t.dataset_review?.semantics?.conflict_badge}\n \n {/if}\n {#if field.needs_review}\n \n {$t.dataset_review?.semantics?.needs_review_badge}\n \n {/if}\n
\n\n
\n
\n {$t.dataset_review?.semantics?.active_value_label}\n
\n
\n {getCurrentValueSummary(field)}\n
\n
\n \n {$t.dataset_review?.semantics?.provenance_label}:\n {getProvenanceLabel(field.provenance)}\n \n \n {$t.dataset_review?.semantics?.confidence_label}:\n {getConfidenceLabel(field.confidence_rank)}\n \n {#if field.source_id}\n \n {$t.dataset_review?.semantics?.source_label}: {getSourceLabel(field.source_id)}\n \n {/if}\n \n {$t.dataset_review?.semantics?.changed_by_label}: {field.last_changed_by}\n \n
\n
\n\n {#if isEditing}\n
\n \n\n \n\n \n\n
\n saveManualOverride(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.save_manual_action}\n \n cancelManualEdit(field.field_id)}\n disabled={isSaving}\n >\n {$t.common?.cancel}\n \n
\n
\n {/if}\n\n
\n
\n

\n {$t.dataset_review?.semantics?.candidates_title}\n

\n \n {field.candidates?.length || 0}\n \n
\n\n {#if !field.candidates?.length}\n

\n {$t.dataset_review?.semantics?.candidates_empty}\n

\n {:else}\n
\n {#each field.candidates as candidate}\n
\n
\n
\n
\n
\n {candidate.proposed_verbose_name ||\n $t.dataset_review?.semantics?.empty_value}\n
\n \n {candidate.match_type}\n \n \n {$t.dataset_review?.semantics?.score_label}:\n {candidate.confidence_score}\n \n \n {getCandidateStatusLabel(candidate.status)}\n \n
\n\n

\n {candidate.proposed_description ||\n $t.dataset_review?.semantics?.candidate_description_empty}\n

\n\n {#if candidate.proposed_display_format}\n

\n {$t.dataset_review?.semantics?.display_format_label}:\n {candidate.proposed_display_format}\n

\n {/if}\n
\n\n
\n acceptCandidate(field, candidate.candidate_id, false)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.apply_candidate_action}\n \n acceptCandidate(field, candidate.candidate_id, true)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.apply_and_lock_action}\n \n
\n
\n
\n {/each}\n
\n {/if}\n
\n\n {#if message.text || message.status === \"saving\"}\n \n {message.status === \"saving\"\n ? $t.dataset_review?.semantics?.messages?.saving\n : message.text}\n
\n {/if}\n
\n\n
\n askAiAboutField(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFieldDraft(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n startManualEdit(field)}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.manual_override_action}\n \n\n mutateLock(field, field.is_locked ? \"unlock\" : \"lock\")}\n disabled={disabled || isSaving}\n >\n {field.is_locked\n ? $t.dataset_review?.semantics?.unlock_action\n : $t.dataset_review?.semantics?.lock_action}\n \n\n
\n recordFeedback(field.field_id, 'up')}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.feedback_up_action}\n \n recordFeedback(field.field_id, 'down')}\n disabled={disabled || isSaving}\n >\n {$t.dataset_review?.semantics?.feedback_down_action}\n \n
\n
\n
\n \n {/each}\n \n {/if}\n
\n\n\n" }, { "contract_id": "SourceIntakePanel", @@ -84981,7 +89962,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

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

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

\n

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

\n

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

\n
\n\n \n {#if intakeState === \"Rejected\"}\n {$t.dataset_review?.source?.state_rejected}\n {:else if intakeState === \"Validating\"}\n {$t.dataset_review?.source?.state_validating}\n {:else}\n {$t.dataset_review?.source?.state_idle}\n {/if}\n
\n
\n\n
\n setMode(\"superset_link\")}\n >\n
\n {$t.dataset_review?.source?.superset_link_tab}\n
\n
\n {$t.dataset_review?.source?.superset_link_tab_hint}\n
\n \n\n setMode(\"dataset_selection\")}\n >\n
\n {$t.dataset_review?.source?.dataset_selection_tab}\n
\n
\n {$t.dataset_review?.source?.dataset_selection_tab_hint}\n
\n \n
\n\n
\n Progress: {progressionLabel}. После старта сессии можно продолжить review даже при частично восстановленном контексте.\n
\n\n
\n
\n \n\n \n
\n\n \n {getInlineHint()}\n \n\n
\n
\n {#if isSupersetLinkMode}\n {$t.dataset_review?.source?.superset_link_recovery_note}\n {:else}\n {$t.dataset_review?.source?.dataset_selection_recovery_note}\n {/if}\n
\n Долгие шаги восстановления выполняются асинхронно: статус и следующий шаг будут видны в workspace.\n
\n
\n\n \n {#if submitting}\n {$t.dataset_review?.source?.submitting}\n {:else if isSupersetLinkMode}\n {$t.dataset_review?.source?.submit_superset_link}\n {:else}\n {$t.dataset_review?.source?.submit_dataset_selection}\n {/if}\n \n
\n \n\n\n\n" }, { "contract_id": "ValidationFindingsPanel", @@ -85577,15 +90558,28 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TYPE value '{{' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{{" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n {#if loading}\n
\n
\n
\n
\n
\n {:else if error}\n
\n

{$t.health?.load_failed}

\n

{error}

\n
\n {:else}\n
\n
\n
🟢 {pass_count}
\n
{$t.health?.status_pass}
\n
\n \n
\n
🟡 {warn_count}
\n
{$t.health?.status_warn}
\n
\n \n
\n
🔴 {fail_count}
\n
{$t.health?.status_fail}
\n
\n\n
\n
⚪ {unknown_count}
\n
{$t.health?.status_unknown}
\n
\n
\n \n {#if total === 0}\n
\n {$t.health?.no_records_for_environment}\n
\n {/if}\n {/if}\n
\n\n\n\n" + "body": "\n\n\n\n\n\n
\n {#if loading}\n
\n
\n
\n
\n
\n {:else if error}\n
\n

{$t.health?.load_failed}

\n

{error}

\n
\n {:else}\n
\n
\n
🟢 {pass_count}
\n
{$t.health?.status_pass}
\n
\n \n
\n
🟡 {warn_count}
\n
{$t.health?.status_warn}
\n
\n \n
\n
🔴 {fail_count}
\n
{$t.health?.status_fail}
\n
\n\n
\n
⚪ {unknown_count}
\n
{$t.health?.status_unknown}
\n
\n
\n \n {#if total === 0}\n
\n {$t.health?.no_records_for_environment}\n
\n {/if}\n {/if}\n
\n\n\n\n" }, { "contract_id": "PolicyForm", @@ -85656,22 +90650,132 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TYPE value '{{ policy: any, environments: any[], onSave: (p: any) => void, onCancel: () => void }}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{{ policy: any, environments: any[], onSave: (p: any) => void, onCancel: () => void }}" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n
\n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n Schedule Days\n
\n {#each days as day}\n \n {/each}\n
\n
\n
\n\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n {#if formData.dashboard_ids.length > 0}\n
\n

\n 💡 System will automatically distribute {formData.dashboard_ids.length} checks within this {Math.floor(windowDurationMinutes() / 60)}h {windowDurationMinutes() % 60}m window.\n

\n {#if isWindowTooSmall}\n

\n ⚠️ Window might be too narrow for {formData.dashboard_ids.length} dashboards.\n

\n {/if}\n
\n {/if}\n\n
\n \n \n
\n\n
\n \n \n
\n
\n
\n\n
\n \n \n
\n
\n\n" + "body": "\n\n\n\n\n\n
\n
\n \n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n Schedule Days\n
\n {#each days as day}\n \n {/each}\n
\n
\n
\n\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n {#if formData.dashboard_ids.length > 0}\n
\n

\n 💡 System will automatically distribute {formData.dashboard_ids.length} checks within this {Math.floor(windowDurationMinutes() / 60)}h {windowDurationMinutes() % 60}m window.\n

\n {#if isWindowTooSmall}\n

\n ⚠️ Window might be too narrow for {formData.dashboard_ids.length} dashboards.\n

\n {/if}\n
\n {/if}\n\n
\n \n \n
\n\n
\n \n \n
\n
\n
\n\n
\n \n \n
\n
\n\n" + }, + { + "contract_id": "Sidebar", + "contract_type": "Component", + "file_path": "frontend/src/lib/components/layout/Sidebar.svelte", + "start_line": 1, + "end_line": 431, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "LAYER": "UI", + "PURPOSE": "Persistent left sidebar with grouped sections (Resources/Operations/System), expand/collapse, health indicators, RBAC filtering, profile footer.", + "UX_FEEDBACK": "Profile footer shows user avatar and username.", + "UX_RECOVERY": "Click outside on mobile closes overlay.", + "UX_STATE": "MobileOpen -> Overlay and sidebar visible until dismissed." + }, + "relations": [ + { + "source_id": "Sidebar", + "relation_type": "BINDS_TO", + "target_id": "sidebarStore", + "target_ref": "[sidebarStore]" + }, + { + "source_id": "Sidebar", + "relation_type": "BINDS_TO", + "target_id": "healthStore", + "target_ref": "[healthStore]" + }, + { + "source_id": "Sidebar", + "relation_type": "BINDS_TO", + "target_id": "authStore", + "target_ref": "[authStore]" + }, + { + "source_id": "Sidebar", + "relation_type": "DEPENDS_ON", + "target_id": "Icon", + "target_ref": "[Icon]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "POST", + "message": "@POST is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "PRE", + "message": "@PRE is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is required for contract type 'Component' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{#if isMobileOpen}\n \n \n{/if}\n\n\n\n \n \n {#if isExpanded}\n \n \n \n \n {translationState.current?.nav?.menu || \"Menu\"}\n \n \n \n \n {:else}\n \n \n \n \n \n \n {/if}\n \n\n \n \n\n \n
\n {#if isExpanded}\n \n
\n \n \n {getInitials(user)}\n \n \n {user?.display_name || user?.username || user?.email || \"Profile\"}\n \n \n
\n \n \n \n
\n
\n {:else}\n \n
\n \n \n {getInitials(user)}\n \n \n \n \n \n
\n {/if}\n
\n\n\n\n" }, { "contract_id": "disconnectWebSocket:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 447, - "end_line": 462, + "start_line": 451, + "end_line": 466, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -85763,8 +90867,8 @@ "contract_id": "loadRecentTasks:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 464, - "end_line": 487, + "start_line": 468, + "end_line": 491, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -85801,8 +90905,8 @@ "contract_id": "selectTask:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 489, - "end_line": 502, + "start_line": 493, + "end_line": 506, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -85839,8 +90943,8 @@ "contract_id": "goBackToList:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 504, - "end_line": 522, + "start_line": 508, + "end_line": 526, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -85933,12 +91037,12 @@ "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/__tests__/sidebarNavigation.test.js", "start_line": 1, - "end_line": 98, + "end_line": 149, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "UI (Tests)", - "PURPOSE": "Verifies RBAC-based sidebar category and subitem visibility.", + "PURPOSE": "Verifies RBAC-based sidebar sections and category visibility.", "SEMANTICS": [ "tests", "sidebar", @@ -86030,7 +91134,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region SidebarNavigationTest:Module [TYPE Function]\n// @SEMANTICS: tests, sidebar, navigation, rbac, permissions\n// @PURPOSE: Verifies RBAC-based sidebar category and subitem visibility.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [SidebarNavigation]\n\nimport { describe, it, expect } from \"vitest\";\nimport { buildSidebarCategories } from \"../sidebarNavigation.js\";\n\nconst i18nState = {\n nav: {\n dashboards: \"Dashboards\",\n overview: \"Overview\",\n datasets: \"Datasets\",\n all_datasets: \"All datasets\",\n storage: \"Storage\",\n backups: \"Backups\",\n repositories: \"Repositories\",\n reports: \"Reports\",\n profile: \"Profile\",\n admin: \"Admin\",\n admin_users: \"User management\",\n admin_roles: \"Role management\",\n settings: \"Settings\",\n },\n};\n\nfunction makeUser(roles) {\n return { roles };\n}\n\ndescribe(\"sidebarNavigation\", () => {\n it(\"shows only categories available to a non-admin user\", () => {\n const user = makeUser([\n {\n name: \"Operator\",\n permissions: [\n { resource: \"plugin:migration\", action: \"READ\" },\n { resource: \"tasks\", action: \"READ\" },\n ],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const categoryIds = categories.map((category) => category.id);\n\n expect(categoryIds).toEqual([\"dashboards\", \"datasets\", \"reports\", \"profile\"]);\n });\n\n it(\"hides admin category when user has no admin permissions\", () => {\n const user = makeUser([\n {\n name: \"Viewer\",\n permissions: [{ resource: \"plugin:migration\", action: \"READ\" }],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const adminCategory = categories.find((category) => category.id === \"admin\");\n\n expect(adminCategory).toBeUndefined();\n });\n\n it(\"shows full admin category for admin role\", () => {\n const user = makeUser([\n {\n name: \"Admin\",\n permissions: [],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const adminCategory = categories.find((category) => category.id === \"admin\");\n\n expect(adminCategory).toBeDefined();\n expect(adminCategory.subItems.map((item) => item.path)).toEqual([\n \"/admin/users\",\n \"/admin/roles\",\n \"/settings\",\n ]);\n });\n\n it(\"keeps profile visible even without explicit plugin permissions\", () => {\n const user = makeUser([\n {\n name: \"Basic\",\n permissions: [],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const categoryIds = categories.map((category) => category.id);\n\n expect(categoryIds).toEqual([\"profile\"]);\n });\n});\n\n// #endregion SidebarNavigationTest:Module\n" + "body": "// #region SidebarNavigationTest:Module [TYPE Function]\n// @SEMANTICS: tests, sidebar, navigation, rbac, permissions\n// @PURPOSE: Verifies RBAC-based sidebar sections and category visibility.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [SidebarNavigation]\n\nimport { describe, it, expect } from \"vitest\";\nimport { buildSidebarCategories, buildSidebarSections } from \"../sidebarNavigation.js\";\n\nconst i18nState = {\n nav: {\n section_resources: \"Resources\",\n section_operations: \"Operations\",\n section_system: \"System\",\n dashboards: \"Dashboards\",\n overview: \"Overview\",\n health_center: \"Health Center\",\n datasets: \"Datasets\",\n all_datasets: \"All Datasets\",\n dataset_review_workspace: \"Review Workspace\",\n migration: \"Migration\",\n migration_overview: \"Overview\",\n migration_mappings: \"Mappings\",\n git: \"Git\",\n git_repo_status: \"Repository Status\",\n translation: \"Translation\",\n translation_jobs: \"Jobs\",\n translation_dictionaries: \"Dictionaries\",\n translation_history: \"History\",\n reports: \"Reports\",\n all_reports: \"All Reports\",\n tools: \"Tools\",\n tools_mapper: \"Dataset Mapper\",\n tools_debug: \"System Debug\",\n tools_storage: \"File Storage\",\n tools_backups: \"Backup Manager\",\n storage: \"Storage\",\n backups: \"Backups\",\n repositories: \"Repositories\",\n settings: \"Settings\",\n admin: \"Admin\",\n admin_users: \"User Management\",\n admin_roles: \"Role Management\",\n admin_settings: \"ADFS Config\",\n admin_llm: \"LLM Providers\",\n profile: \"Profile\",\n },\n};\n\nfunction makeUser(roles) {\n return { roles };\n}\n\ndescribe(\"sidebarNavigation\", () => {\n it(\"shows sections structure with correct section IDs\", () => {\n // Admin user sees all sections\n const user = makeUser([\n {\n name: \"Admin\",\n permissions: [],\n },\n ]);\n const sections = buildSidebarSections(i18nState, user);\n const sectionIds = sections.map((s) => s.id);\n\n expect(sectionIds).toEqual([\"resources\", \"operations\", \"system\"]);\n });\n\n it(\"shows only categories available to a non-admin user\", () => {\n const user = makeUser([\n {\n name: \"Operator\",\n permissions: [\n { resource: \"plugin:migration\", action: \"READ\" },\n { resource: \"tasks\", action: \"READ\" },\n ],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const categoryIds = categories.map((category) => category.id);\n\n // migration requires plugin:migration → visible\n // git/tools have no requiredPermission → always visible\n // translation requires translate.job → hidden\n // storage requires plugin:storage → hidden\n // admin requires admin:* → hidden\n expect(categoryIds).toEqual([\n \"dashboards\",\n \"datasets\",\n \"migration\",\n \"git\",\n \"reports\",\n \"tools\",\n ]);\n });\n\n it(\"hides admin category when user has no admin permissions\", () => {\n const user = makeUser([\n {\n name: \"Viewer\",\n permissions: [{ resource: \"plugin:migration\", action: \"READ\" }],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const adminCategory = categories.find((category) => category.id === \"admin\");\n\n expect(adminCategory).toBeUndefined();\n });\n\n it(\"shows full admin category for admin role\", () => {\n const user = makeUser([\n {\n name: \"Admin\",\n permissions: [],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const adminCategory = categories.find((category) => category.id === \"admin\");\n\n expect(adminCategory).toBeDefined();\n expect(adminCategory.subItems.map((item) => item.path)).toEqual([\n \"/admin/users\",\n \"/admin/roles\",\n \"/admin/settings\",\n \"/admin/settings/llm\",\n ]);\n });\n\n it(\"shows git and tools for users without plugin permissions\", () => {\n const user = makeUser([\n {\n name: \"Basic\",\n permissions: [],\n },\n ]);\n\n const categories = buildSidebarCategories(i18nState, user);\n const categoryIds = categories.map((category) => category.id);\n\n // Profile is now in the sidebar footer, not a nav category\n // Git and Tools have no requiredPermission → always visible\n expect(categoryIds).toContain(\"git\");\n expect(categoryIds).toContain(\"tools\");\n });\n});\n// #endregion SidebarNavigationTest:Module\n" }, { "contract_id": "BreadcrumbsContractTest:Module", @@ -86349,13 +91453,13 @@ "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/sidebarNavigation.js", "start_line": 1, - "end_line": 213, + "end_line": 204, "tier": "TIER_1", "complexity": 1, "metadata": { "INVARIANT": "Admin role can access all categories and subitems through permission utility.", "LAYER": "UI", - "PURPOSE": "Build sidebar navigation categories filtered by current user permissions.", + "PURPOSE": "Build sidebar navigation sections and categories filtered by user permissions and feature flags.", "SEMANTICS": [ "navigation", "sidebar", @@ -86445,7 +91549,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region SidebarNavigation:Module [TYPE Function]\n// @SEMANTICS: navigation, sidebar, rbac, menu, filtering\n// @PURPOSE: Build sidebar navigation categories filtered by current user permissions.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [Permissions]\n// @RELATION: BINDS_TO -> [Sidebar]\n// @INVARIANT: Admin role can access all categories and subitems through permission utility.\n\nimport { hasPermission } from \"$lib/auth/permissions.js\";\n\n// #region isItemAllowed:Function [TYPE Function]\n// @PURPOSE: Check whether a single menu node can be shown for a given user.\n// @PRE: item can contain optional requiredPermission/requiredAction.\n// @POST: Returns true when no permission is required or permission check passes.\nfunction isItemAllowed(user, item) {\n if (!item?.requiredPermission) return true;\n return hasPermission(\n user,\n item.requiredPermission,\n item.requiredAction || \"READ\",\n );\n}\n// #endregion isItemAllowed:Function\n\n// #region buildSidebarCategories:Function [TYPE Function]\n// @PURPOSE: Build translated sidebar categories and filter them by RBAC permissions and feature flags.\n// @PRE: i18nState provides nav labels; user can be null; features default to {} (all enabled).\n// @POST: Returns only categories/subitems available for provided user and enabled features.\nexport function buildSidebarCategories(i18nState, user, features = {}) {\n const nav = i18nState?.nav || {};\n\n const categories = [\n {\n id: \"dashboards\",\n label: nav.dashboards,\n icon: \"dashboard\",\n tone: \"from-sky-100 to-sky-200 text-sky-700 ring-sky-200\",\n path: \"/dashboards\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.overview,\n path: \"/dashboards\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n },\n {\n label: nav.health_center,\n path: \"/dashboards/health\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n requiredFeature: \"health_monitor\",\n },\n ],\n },\n {\n id: \"datasets\",\n label: nav.datasets,\n icon: \"database\",\n tone: \"from-emerald-100 to-emerald-200 text-emerald-700 ring-emerald-200\",\n path: \"/datasets\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.all_datasets,\n path: \"/datasets\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n },\n {\n label: nav.dataset_review_workspace,\n path: \"/datasets/review\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n requiredFeature: \"dataset_review\",\n },\n ],\n },\n {\n id: \"storage\",\n label: nav.storage,\n icon: \"storage\",\n tone: \"from-amber-100 to-amber-200 text-amber-800 ring-amber-200\",\n path: \"/storage\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.backups,\n path: \"/storage/backups\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n },\n {\n label: nav.repositories,\n path: \"/storage/repos\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n },\n ],\n },\n {\n id: \"translation\",\n label: nav.translation,\n icon: \"translate\",\n tone: \"from-cyan-100 to-teal-100 text-teal-700 ring-teal-200\",\n path: \"/translate\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n subItems: [\n {\n label: nav.translation_jobs,\n path: \"/translate\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n },\n {\n label: nav.translation_dictionaries,\n path: \"/translate/dictionaries\",\n requiredPermission: \"translate.dictionary\",\n requiredAction: \"VIEW\",\n },\n {\n label: nav.translation_history,\n path: \"/translate/history\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n },\n ],\n },\n {\n id: \"reports\",\n label: nav.reports,\n icon: \"reports\",\n tone: \"from-violet-100 to-fuchsia-100 text-violet-700 ring-violet-200\",\n path: \"/reports\",\n requiredPermission: \"tasks\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.reports,\n path: \"/reports\",\n requiredPermission: \"tasks\",\n requiredAction: \"READ\",\n },\n ],\n },\n {\n id: \"profile\",\n label: nav.profile,\n icon: \"admin\",\n tone: \"from-indigo-100 to-indigo-200 text-indigo-700 ring-indigo-200\",\n path: \"/profile\",\n subItems: [{ label: nav.profile, path: \"/profile\" }],\n },\n {\n id: \"admin\",\n label: nav.admin,\n icon: \"admin\",\n tone: \"from-rose-100 to-rose-200 text-rose-700 ring-rose-200\",\n path: \"/admin\",\n subItems: [\n {\n label: nav.admin_users,\n path: \"/admin/users\",\n requiredPermission: \"admin:users\",\n requiredAction: \"READ\",\n },\n {\n label: nav.admin_roles,\n path: \"/admin/roles\",\n requiredPermission: \"admin:roles\",\n requiredAction: \"READ\",\n },\n {\n label: nav.settings,\n path: \"/settings\",\n requiredPermission: \"admin:settings\",\n requiredAction: \"READ\",\n },\n ],\n },\n ];\n\n function isFeatureEnabled(item) {\n if (!item?.requiredFeature) return true;\n return features[item.requiredFeature] !== false;\n }\n\n return categories\n .map((category) => {\n const visibleSubItems = (category.subItems || []).filter(\n (subItem) => isItemAllowed(user, subItem) && isFeatureEnabled(subItem),\n );\n return {\n ...category,\n subItems: visibleSubItems,\n };\n })\n .filter((category) => {\n const categoryVisible = isItemAllowed(user, category);\n if (!categoryVisible) return false;\n\n const hasVisibleSubItems =\n Array.isArray(category.subItems) && category.subItems.length > 0;\n return hasVisibleSubItems;\n });\n}\n// #endregion buildSidebarCategories:Function\n\n// #endregion SidebarNavigation:Module\n" + "body": "// #region SidebarNavigation:Module [TYPE Function]\n// @SEMANTICS: navigation, sidebar, rbac, menu, filtering\n// @PURPOSE: Build sidebar navigation sections and categories filtered by user permissions and feature flags.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [Permissions]\n// @RELATION: BINDS_TO -> [Sidebar]\n// @INVARIANT: Admin role can access all categories and subitems through permission utility.\n\nimport { hasPermission } from \"$lib/auth/permissions.js\";\n\n// #region isItemAllowed:Function [TYPE Function]\n// @PURPOSE: Check whether a single menu node can be shown for a given user.\n// @PRE: item can contain optional requiredPermission/requiredAction.\n// @POST: Returns true when no permission is required or permission check passes.\nfunction isItemAllowed(user, item) {\n if (!item?.requiredPermission) return true;\n return hasPermission(\n user,\n item.requiredPermission,\n item.requiredAction || \"READ\",\n );\n}\n// #endregion isItemAllowed:Function\n\n// #region isFeatureEnabled:Function [TYPE Function]\nfunction isFeatureEnabled(item, features) {\n if (!item?.requiredFeature) return true;\n return features[item.requiredFeature] !== false;\n}\n// #endregion isFeatureEnabled:Function\n\n// #region buildSidebarSections:Function [TYPE Function]\n// @PURPOSE: Build translated sidebar sections with categories, filtered by RBAC permissions and feature flags.\n// @PRE: i18nState provides nav labels; user can be null; features default to {} (all enabled).\n// @POST: Returns only sections/categories/subitems available for provided user and enabled features.\nexport function buildSidebarSections(i18nState, user, features = {}) {\n const nav = i18nState?.nav || {};\n\n // ── Section 1: Resources ─────────────────────────────────\n const resources = {\n id: \"resources\",\n label: nav.section_resources,\n categories: [\n {\n id: \"dashboards\",\n label: nav.dashboards,\n icon: \"dashboard\",\n tone: \"from-sky-100 to-sky-200 text-sky-700 ring-sky-200\",\n path: \"/dashboards\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.overview, path: \"/dashboards\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n { label: nav.health_center, path: \"/dashboards/health\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\", requiredFeature: \"health_monitor\" },\n ],\n },\n {\n id: \"datasets\",\n label: nav.datasets,\n icon: \"database\",\n tone: \"from-emerald-100 to-emerald-200 text-emerald-700 ring-emerald-200\",\n path: \"/datasets\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.all_datasets, path: \"/datasets\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n { label: nav.dataset_review_workspace, path: \"/datasets/review\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\", requiredFeature: \"dataset_review\" },\n ],\n },\n {\n id: \"migration\",\n label: nav.migration,\n icon: \"layers\",\n tone: \"from-orange-100 to-orange-200 text-orange-700 ring-orange-200\",\n path: \"/migration\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.migration_overview || \"Overview\", path: \"/migration\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n { label: nav.migration_mappings || \"Mappings\", path: \"/migration/mappings\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n ],\n },\n ],\n };\n\n // ── Section 2: Operations ────────────────────────────────\n const operations = {\n id: \"operations\",\n label: nav.section_operations,\n categories: [\n {\n id: \"git\",\n label: nav.git,\n icon: \"activity\",\n tone: \"from-purple-100 to-purple-200 text-purple-700 ring-purple-200\",\n path: \"/git\",\n subItems: [\n { label: nav.git_repo_status || \"Repository Status\", path: \"/git\" },\n ],\n },\n {\n id: \"translation\",\n label: nav.translation,\n icon: \"translate\",\n tone: \"from-cyan-100 to-teal-100 text-teal-700 ring-teal-200\",\n path: \"/translate\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n subItems: [\n { label: nav.translation_jobs, path: \"/translate\", requiredPermission: \"translate.job\", requiredAction: \"VIEW\" },\n { label: nav.translation_dictionaries, path: \"/translate/dictionaries\", requiredPermission: \"translate.dictionary\", requiredAction: \"VIEW\" },\n { label: nav.translation_history, path: \"/translate/history\", requiredPermission: \"translate.job\", requiredAction: \"VIEW\" },\n ],\n },\n {\n id: \"reports\",\n label: nav.reports,\n icon: \"reports\",\n tone: \"from-violet-100 to-fuchsia-100 text-violet-700 ring-violet-200\",\n path: \"/reports\",\n requiredPermission: \"tasks\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.all_reports || \"All Reports\", path: \"/reports\", requiredPermission: \"tasks\", requiredAction: \"READ\" },\n ],\n },\n ],\n };\n\n // ── Section 3: System ────────────────────────────────────\n const system = {\n id: \"system\",\n label: nav.section_system,\n categories: [\n {\n id: \"tools\",\n label: nav.tools,\n icon: \"list\",\n tone: \"from-slate-100 to-slate-200 text-slate-700 ring-slate-200\",\n path: \"/tools/mapper\",\n subItems: [\n { label: nav.tools_mapper, path: \"/tools/mapper\" },\n { label: nav.tools_debug, path: \"/tools/debug\" },\n { label: nav.tools_storage, path: \"/tools/storage\" },\n { label: nav.tools_backups, path: \"/tools/backups\" },\n ],\n },\n {\n id: \"storage\",\n label: nav.storage,\n icon: \"storage\",\n tone: \"from-amber-100 to-amber-200 text-amber-800 ring-amber-200\",\n path: \"/storage\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.repositories, path: \"/storage/repos\", requiredPermission: \"plugin:storage\", requiredAction: \"READ\" },\n ],\n },\n {\n id: \"admin\",\n label: nav.admin,\n icon: \"admin\",\n tone: \"from-rose-100 to-rose-200 text-rose-700 ring-rose-200\",\n path: \"/admin/users\",\n subItems: [\n { label: nav.admin_users, path: \"/admin/users\", requiredPermission: \"admin:users\", requiredAction: \"READ\" },\n { label: nav.admin_roles, path: \"/admin/roles\", requiredPermission: \"admin:roles\", requiredAction: \"READ\" },\n { label: nav.admin_settings, path: \"/admin/settings\", requiredPermission: \"admin:settings\", requiredAction: \"READ\" },\n { label: nav.admin_llm, path: \"/admin/settings/llm\", requiredPermission: \"admin:settings\", requiredAction: \"READ\" },\n ],\n },\n ],\n };\n\n const allSections = [resources, operations, system];\n\n // Filter by permissions and features\n return allSections\n .map((section) => ({\n ...section,\n categories: section.categories\n .map((category) => {\n const visibleSubItems = (category.subItems || []).filter(\n (sub) => isItemAllowed(user, sub) && isFeatureEnabled(sub, features),\n );\n return { ...category, subItems: visibleSubItems };\n })\n .filter((category) => {\n if (!isItemAllowed(user, category)) return false;\n return Array.isArray(category.subItems) && category.subItems.length > 0;\n }),\n }))\n .filter((section) => section.categories.length > 0);\n}\n// #endregion buildSidebarSections:Function\n\n// ── Legacy adapter for backward compatibility ──────────────\n// @DEPRECATED Use buildSidebarSections instead. Returns flat category list for old consumers.\nexport function buildSidebarCategories(i18nState, user, features = {}) {\n const sections = buildSidebarSections(i18nState, user, features);\n return sections.flatMap((s) => s.categories);\n}\n// #endregion SidebarNavigation:Module\n" }, { "contract_id": "isItemAllowed:Function", @@ -86486,17 +91590,32 @@ "body": "// #region isItemAllowed:Function [TYPE Function]\n// @PURPOSE: Check whether a single menu node can be shown for a given user.\n// @PRE: item can contain optional requiredPermission/requiredAction.\n// @POST: Returns true when no permission is required or permission check passes.\nfunction isItemAllowed(user, item) {\n if (!item?.requiredPermission) return true;\n return hasPermission(\n user,\n item.requiredPermission,\n item.requiredAction || \"READ\",\n );\n}\n// #endregion isItemAllowed:Function\n" }, { - "contract_id": "buildSidebarCategories:Function", + "contract_id": "isFeatureEnabled:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/sidebarNavigation.js", "start_line": 25, - "end_line": 211, + "end_line": 30, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region isFeatureEnabled:Function [TYPE Function]\nfunction isFeatureEnabled(item, features) {\n if (!item?.requiredFeature) return true;\n return features[item.requiredFeature] !== false;\n}\n// #endregion isFeatureEnabled:Function\n" + }, + { + "contract_id": "buildSidebarSections:Function", + "contract_type": "Function", + "file_path": "frontend/src/lib/components/layout/sidebarNavigation.js", + "start_line": 32, + "end_line": 196, "tier": "TIER_1", "complexity": 1, "metadata": { - "POST": "Returns only categories/subitems available for provided user and enabled features.", + "POST": "Returns only sections/categories/subitems available for provided user and enabled features.", "PRE": "i18nState provides nav labels; user can be null; features default to {} (all enabled).", - "PURPOSE": "Build translated sidebar categories and filter them by RBAC permissions and feature flags." + "PURPOSE": "Build translated sidebar sections with categories, filtered by RBAC permissions and feature flags." }, "relations": [], "schema_warnings": [ @@ -86521,7 +91640,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region buildSidebarCategories:Function [TYPE Function]\n// @PURPOSE: Build translated sidebar categories and filter them by RBAC permissions and feature flags.\n// @PRE: i18nState provides nav labels; user can be null; features default to {} (all enabled).\n// @POST: Returns only categories/subitems available for provided user and enabled features.\nexport function buildSidebarCategories(i18nState, user, features = {}) {\n const nav = i18nState?.nav || {};\n\n const categories = [\n {\n id: \"dashboards\",\n label: nav.dashboards,\n icon: \"dashboard\",\n tone: \"from-sky-100 to-sky-200 text-sky-700 ring-sky-200\",\n path: \"/dashboards\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.overview,\n path: \"/dashboards\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n },\n {\n label: nav.health_center,\n path: \"/dashboards/health\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n requiredFeature: \"health_monitor\",\n },\n ],\n },\n {\n id: \"datasets\",\n label: nav.datasets,\n icon: \"database\",\n tone: \"from-emerald-100 to-emerald-200 text-emerald-700 ring-emerald-200\",\n path: \"/datasets\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.all_datasets,\n path: \"/datasets\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n },\n {\n label: nav.dataset_review_workspace,\n path: \"/datasets/review\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n requiredFeature: \"dataset_review\",\n },\n ],\n },\n {\n id: \"storage\",\n label: nav.storage,\n icon: \"storage\",\n tone: \"from-amber-100 to-amber-200 text-amber-800 ring-amber-200\",\n path: \"/storage\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.backups,\n path: \"/storage/backups\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n },\n {\n label: nav.repositories,\n path: \"/storage/repos\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n },\n ],\n },\n {\n id: \"translation\",\n label: nav.translation,\n icon: \"translate\",\n tone: \"from-cyan-100 to-teal-100 text-teal-700 ring-teal-200\",\n path: \"/translate\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n subItems: [\n {\n label: nav.translation_jobs,\n path: \"/translate\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n },\n {\n label: nav.translation_dictionaries,\n path: \"/translate/dictionaries\",\n requiredPermission: \"translate.dictionary\",\n requiredAction: \"VIEW\",\n },\n {\n label: nav.translation_history,\n path: \"/translate/history\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n },\n ],\n },\n {\n id: \"reports\",\n label: nav.reports,\n icon: \"reports\",\n tone: \"from-violet-100 to-fuchsia-100 text-violet-700 ring-violet-200\",\n path: \"/reports\",\n requiredPermission: \"tasks\",\n requiredAction: \"READ\",\n subItems: [\n {\n label: nav.reports,\n path: \"/reports\",\n requiredPermission: \"tasks\",\n requiredAction: \"READ\",\n },\n ],\n },\n {\n id: \"profile\",\n label: nav.profile,\n icon: \"admin\",\n tone: \"from-indigo-100 to-indigo-200 text-indigo-700 ring-indigo-200\",\n path: \"/profile\",\n subItems: [{ label: nav.profile, path: \"/profile\" }],\n },\n {\n id: \"admin\",\n label: nav.admin,\n icon: \"admin\",\n tone: \"from-rose-100 to-rose-200 text-rose-700 ring-rose-200\",\n path: \"/admin\",\n subItems: [\n {\n label: nav.admin_users,\n path: \"/admin/users\",\n requiredPermission: \"admin:users\",\n requiredAction: \"READ\",\n },\n {\n label: nav.admin_roles,\n path: \"/admin/roles\",\n requiredPermission: \"admin:roles\",\n requiredAction: \"READ\",\n },\n {\n label: nav.settings,\n path: \"/settings\",\n requiredPermission: \"admin:settings\",\n requiredAction: \"READ\",\n },\n ],\n },\n ];\n\n function isFeatureEnabled(item) {\n if (!item?.requiredFeature) return true;\n return features[item.requiredFeature] !== false;\n }\n\n return categories\n .map((category) => {\n const visibleSubItems = (category.subItems || []).filter(\n (subItem) => isItemAllowed(user, subItem) && isFeatureEnabled(subItem),\n );\n return {\n ...category,\n subItems: visibleSubItems,\n };\n })\n .filter((category) => {\n const categoryVisible = isItemAllowed(user, category);\n if (!categoryVisible) return false;\n\n const hasVisibleSubItems =\n Array.isArray(category.subItems) && category.subItems.length > 0;\n return hasVisibleSubItems;\n });\n}\n// #endregion buildSidebarCategories:Function\n" + "body": "// #region buildSidebarSections:Function [TYPE Function]\n// @PURPOSE: Build translated sidebar sections with categories, filtered by RBAC permissions and feature flags.\n// @PRE: i18nState provides nav labels; user can be null; features default to {} (all enabled).\n// @POST: Returns only sections/categories/subitems available for provided user and enabled features.\nexport function buildSidebarSections(i18nState, user, features = {}) {\n const nav = i18nState?.nav || {};\n\n // ── Section 1: Resources ─────────────────────────────────\n const resources = {\n id: \"resources\",\n label: nav.section_resources,\n categories: [\n {\n id: \"dashboards\",\n label: nav.dashboards,\n icon: \"dashboard\",\n tone: \"from-sky-100 to-sky-200 text-sky-700 ring-sky-200\",\n path: \"/dashboards\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.overview, path: \"/dashboards\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n { label: nav.health_center, path: \"/dashboards/health\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\", requiredFeature: \"health_monitor\" },\n ],\n },\n {\n id: \"datasets\",\n label: nav.datasets,\n icon: \"database\",\n tone: \"from-emerald-100 to-emerald-200 text-emerald-700 ring-emerald-200\",\n path: \"/datasets\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.all_datasets, path: \"/datasets\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n { label: nav.dataset_review_workspace, path: \"/datasets/review\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\", requiredFeature: \"dataset_review\" },\n ],\n },\n {\n id: \"migration\",\n label: nav.migration,\n icon: \"layers\",\n tone: \"from-orange-100 to-orange-200 text-orange-700 ring-orange-200\",\n path: \"/migration\",\n requiredPermission: \"plugin:migration\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.migration_overview || \"Overview\", path: \"/migration\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n { label: nav.migration_mappings || \"Mappings\", path: \"/migration/mappings\", requiredPermission: \"plugin:migration\", requiredAction: \"READ\" },\n ],\n },\n ],\n };\n\n // ── Section 2: Operations ────────────────────────────────\n const operations = {\n id: \"operations\",\n label: nav.section_operations,\n categories: [\n {\n id: \"git\",\n label: nav.git,\n icon: \"activity\",\n tone: \"from-purple-100 to-purple-200 text-purple-700 ring-purple-200\",\n path: \"/git\",\n subItems: [\n { label: nav.git_repo_status || \"Repository Status\", path: \"/git\" },\n ],\n },\n {\n id: \"translation\",\n label: nav.translation,\n icon: \"translate\",\n tone: \"from-cyan-100 to-teal-100 text-teal-700 ring-teal-200\",\n path: \"/translate\",\n requiredPermission: \"translate.job\",\n requiredAction: \"VIEW\",\n subItems: [\n { label: nav.translation_jobs, path: \"/translate\", requiredPermission: \"translate.job\", requiredAction: \"VIEW\" },\n { label: nav.translation_dictionaries, path: \"/translate/dictionaries\", requiredPermission: \"translate.dictionary\", requiredAction: \"VIEW\" },\n { label: nav.translation_history, path: \"/translate/history\", requiredPermission: \"translate.job\", requiredAction: \"VIEW\" },\n ],\n },\n {\n id: \"reports\",\n label: nav.reports,\n icon: \"reports\",\n tone: \"from-violet-100 to-fuchsia-100 text-violet-700 ring-violet-200\",\n path: \"/reports\",\n requiredPermission: \"tasks\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.all_reports || \"All Reports\", path: \"/reports\", requiredPermission: \"tasks\", requiredAction: \"READ\" },\n ],\n },\n ],\n };\n\n // ── Section 3: System ────────────────────────────────────\n const system = {\n id: \"system\",\n label: nav.section_system,\n categories: [\n {\n id: \"tools\",\n label: nav.tools,\n icon: \"list\",\n tone: \"from-slate-100 to-slate-200 text-slate-700 ring-slate-200\",\n path: \"/tools/mapper\",\n subItems: [\n { label: nav.tools_mapper, path: \"/tools/mapper\" },\n { label: nav.tools_debug, path: \"/tools/debug\" },\n { label: nav.tools_storage, path: \"/tools/storage\" },\n { label: nav.tools_backups, path: \"/tools/backups\" },\n ],\n },\n {\n id: \"storage\",\n label: nav.storage,\n icon: \"storage\",\n tone: \"from-amber-100 to-amber-200 text-amber-800 ring-amber-200\",\n path: \"/storage\",\n requiredPermission: \"plugin:storage\",\n requiredAction: \"READ\",\n subItems: [\n { label: nav.repositories, path: \"/storage/repos\", requiredPermission: \"plugin:storage\", requiredAction: \"READ\" },\n ],\n },\n {\n id: \"admin\",\n label: nav.admin,\n icon: \"admin\",\n tone: \"from-rose-100 to-rose-200 text-rose-700 ring-rose-200\",\n path: \"/admin/users\",\n subItems: [\n { label: nav.admin_users, path: \"/admin/users\", requiredPermission: \"admin:users\", requiredAction: \"READ\" },\n { label: nav.admin_roles, path: \"/admin/roles\", requiredPermission: \"admin:roles\", requiredAction: \"READ\" },\n { label: nav.admin_settings, path: \"/admin/settings\", requiredPermission: \"admin:settings\", requiredAction: \"READ\" },\n { label: nav.admin_llm, path: \"/admin/settings/llm\", requiredPermission: \"admin:settings\", requiredAction: \"READ\" },\n ],\n },\n ],\n };\n\n const allSections = [resources, operations, system];\n\n // Filter by permissions and features\n return allSections\n .map((section) => ({\n ...section,\n categories: section.categories\n .map((category) => {\n const visibleSubItems = (category.subItems || []).filter(\n (sub) => isItemAllowed(user, sub) && isFeatureEnabled(sub, features),\n );\n return { ...category, subItems: visibleSubItems };\n })\n .filter((category) => {\n if (!isItemAllowed(user, category)) return false;\n return Array.isArray(category.subItems) && category.subItems.length > 0;\n }),\n }))\n .filter((section) => section.categories.length > 0);\n}\n// #endregion buildSidebarSections:Function\n" }, { "contract_id": "ReportCardTest:Module", @@ -87370,12 +92489,13 @@ "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/BulkCorrectionSidebar.svelte", "start_line": 1, - "end_line": 301, + "end_line": 285, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Sidebar for collecting multiple incorrect terms across different rows,", + "PURPOSE": "Component component: lib/components/translate/BulkCorrectionSidebar.svelte", + "TYPE": "{'closed'|'collecting'|'reviewing'|'submitting'|'submitted'}", "UX_FEEDBACK": "Toast on success/error; count badge on toggle button", "UX_RECOVERY": "Remove individual items; retry submission", "UX_STATE": "submitted -> Success feedback" @@ -87394,22 +92514,44 @@ "target_ref": "[dictionaryApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'closed'|'collecting'|'reviewing'|'submitting'|'submitted'}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'closed'|'collecting'|'reviewing'|'submitting'|'submitted'}" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n \n \n {$t.translate?.corrections?.title}\n {#if items.length > 0}\n \n {items.length}\n \n {/if}\n\n\n\n{#if isOpen}\n
\n\n
\n \n
\n
\n

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

\n

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

\n
\n \n
\n\n \n
\n {#if uxState === 'collecting'}\n {#if items.length === 0}\n
\n \n \n \n

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

\n

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

\n
\n {:else}\n {#each items as item, idx}\n
\n
\n
\n

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

\n

{item.sourceTerm}

\n
\n removeItem(idx)}\n class=\"ml-2 p-0.5 text-gray-400 hover:text-red-500 rounded\"\n title={$t.translate?.corrections?.remove}\n >\n \n \n \n \n
\n
\n

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

\n

{item.incorrectTarget}

\n
\n
\n \n updateCorrection(idx, e.target.value)}\n placeholder={$t.translate?.corrections?.corrected_placeholder}\n class=\"w-full mt-1 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500\"\n />\n
\n
\n {/each}\n {/if}\n\n {:else if uxState === 'submitting'}\n
\n
\n

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

\n
\n\n {:else if uxState === 'submitted'}\n
\n \n \n \n

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

\n {#if submitResult}\n

\n {$t.translate?.corrections?.submit_partial\n ?.replace('{success}', submitResult.created_count || submitResult.updated_count || items.length)\n ?.replace('{total}', items.length)}\n

\n {/if}\n
\n {/if}\n
\n\n \n
\n {#if uxState === 'collecting' && items.length > 0}\n \n
\n \n \n \n {#each dictionaries as dict}\n \n {/each}\n \n
\n\n \n {$t.translate?.corrections?.submit_all?.replace('{count}', validItemCount)}\n \n {:else if uxState === 'submitted'}\n \n {$t.translate?.common?.close}\n \n {/if}\n
\n
\n{/if}\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n \n \n {$t.translate?.corrections?.title}\n {#if items.length > 0}\n \n {items.length}\n \n {/if}\n\n\n\n{#if isOpen}\n
\n\n
\n \n
\n
\n

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

\n

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

\n
\n \n
\n\n \n
\n {#if uxState === 'collecting'}\n {#if items.length === 0}\n
\n \n \n \n

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

\n

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

\n
\n {:else}\n {#each items as item, idx}\n
\n
\n
\n

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

\n

{item.sourceTerm}

\n
\n removeItem(idx)}\n class=\"ml-2 p-0.5 text-gray-400 hover:text-red-500 rounded\"\n title={$t.translate?.corrections?.remove}\n >\n \n \n \n \n
\n
\n

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

\n

{item.incorrectTarget}

\n
\n
\n \n updateCorrection(idx, e.target.value)}\n placeholder={$t.translate?.corrections?.corrected_placeholder}\n class=\"w-full mt-1 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-green-500 focus:border-green-500\"\n />\n
\n
\n {/each}\n {/if}\n\n {:else if uxState === 'submitting'}\n
\n
\n

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

\n
\n\n {:else if uxState === 'submitted'}\n
\n \n \n \n

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

\n {#if submitResult}\n

\n {$t.translate?.corrections?.submit_partial\n ?.replace('{success}', submitResult.created_count || submitResult.updated_count || items.length)\n ?.replace('{total}', items.length)}\n

\n {/if}\n
\n {/if}\n
\n\n \n
\n {#if uxState === 'collecting' && items.length > 0}\n \n
\n \n \n \n {#each dictionaries as dict}\n \n {/each}\n \n
\n\n \n {$t.translate?.corrections?.submit_all?.replace('{count}', validItemCount)}\n \n {:else if uxState === 'submitted'}\n \n {$t.translate?.common?.close}\n \n {/if}\n
\n
\n{/if}\n\n" }, { "contract_id": "BulkReplaceModal", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/BulkReplaceModal.svelte", "start_line": 1, - "end_line": 379, + "end_line": 396, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "LAYER": "UI", "PURPOSE": "Modal dialog for bulk find-and-replace on translated values within a completed run.", + "TYPE": "{'closed'|'configuring'|'previewing'|'preview_error'|'confirming'|'applying'|'applied'|'apply_error'}", "UX_STATE": "apply_error -> Apply failed with error message" }, "relations": [ @@ -87420,30 +92562,74 @@ "target_ref": "[TranslateApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'closed'|'configuring'|'previewing'|'preview_error'|'confirming'|'applying'|'applied'|'apply_error'}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'closed'|'configuring'|'previewing'|'preview_error'|'confirming'|'applying'|'applied'|'apply_error'}" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\tlet uxState = $state('closed');\n\n\tlet findPattern = $state('');\n\tlet replacementText = $state('');\n\tlet isRegex = $state(false);\n\tlet targetLanguage = $state('');\n\tlet affectedRecords = $state([]);\n\tlet applyCount = $state(0);\n\tlet errorMessage = $state('');\n\tlet submitToDict = $state(false);\n\tlet usageNotes = $state('');\n\n\t$effect(() => {\n\t\tif (show) {\n\t\t\tuxState = 'configuring';\n\t\t\tfindPattern = '';\n\t\t\treplacementText = '';\n\t\t\tisRegex = false;\n\t\t\ttargetLanguage = '';\n\t\t\taffectedRecords = [];\n\t\t\tapplyCount = 0;\n\t\t\terrorMessage = '';\n\t\t\tsubmitToDict = false;\n\t\t\tusageNotes = '';\n\t\t} else {\n\t\t\tuxState = 'closed';\n\t\t}\n\t});\n\n\t/** Preview affected records */\n\tasync function handlePreview() {\n\t\tif (!findPattern.trim()) {\n\t\t\taddToast(_('translate.bulk_replace.find_pattern_required'), 'warning');\n\t\t\treturn;\n\t\t}\n\t\tif (!targetLanguage) {\n\t\t\taddToast(_('translate.bulk_replace.target_language_required'), 'warning');\n\t\t\treturn;\n\t\t}\n\t\tuxState = 'previewing';\n\t\terrorMessage = '';\n\t\ttry {\n\t\t\tconst result = await bulkReplacePreview(runId, {\n\t\t\t\tpattern: findPattern,\n\t\t\t\tis_regex: isRegex,\n\t\t\t\treplacement_text: replacementText,\n\t\t\t\ttarget_language: targetLanguage\n\t\t\t});\n\t\t\taffectedRecords = result?.items || result?.records || [];\n\t\t\tapplyCount = affectedRecords.length;\n\t\t\tuxState = 'previewing';\n\t\t} catch (err) {\n\t\t\terrorMessage = err?.message || _('translate.bulk_replace.preview_failed');\n\t\t\tuxState = 'preview_error';\n\t\t\taddToast(errorMessage, 'error');\n\t\t}\n\t}\n\n\t/** Open confirmation */\n\tfunction handleShowConfirm() {\n\t\tuxState = 'confirming';\n\t}\n\n\t/** Cancel confirmation, back to preview */\n\tfunction handleCancelConfirm() {\n\t\tuxState = 'previewing';\n\t}\n\n\t/** Apply the bulk replacement */\n\tasync function handleApply() {\n\t\tuxState = 'applying';\n\t\ttry {\n\t\t\tconst result = await bulkFindReplace(runId, {\n\t\t\t\tpattern: findPattern,\n\t\t\t\tis_regex: isRegex,\n\t\t\t\treplacement_text: replacementText,\n\t\t\t\ttarget_language: targetLanguage,\n\t\t\t\tsubmit_to_dictionary: submitToDict,\n\t\t\t\tusage_notes: usageNotes || undefined,\n\t\t\t\tsubmit_to_dictionary_with_context: submitToDict\n\t\t\t});\n\t\t\tconst changed = result?.changed || result?.affected || applyCount;\n\t\t\tuxState = 'applied';\n\t\t\tonApplied(changed);\n\t\t\taddToast(_('translate.bulk_replace.applied_toast').replace('{count}', changed), 'success');\n\t\t} catch (err) {\n\t\t\terrorMessage = err?.message || _('translate.bulk_replace.apply_failed');\n\t\t\tuxState = 'apply_error';\n\t\t\taddToast(errorMessage, 'error');\n\t\t}\n\t}\n\n\tfunction handleClose() {\n\t\tonClose();\n\t}\n\n\tfunction previewAfter(row) {\n\t\tif (!replacementText && !isRegex) return '';\n\t\tif (isRegex) {\n\t\t\ttry {\n\t\t\t\tconst re = new RegExp(findPattern, 'g');\n\t\t\t\treturn (row.final_value || row.translated_value || '').replace(re, replacementText);\n\t\t\t} catch {\n\t\t\t\treturn row.final_value || row.translated_value || '';\n\t\t\t}\n\t\t}\n\t\treturn (row.final_value || row.translated_value || '').split(findPattern).join(replacementText);\n\t}\n\n\n{#if show}\n\t\n\t\t e.stopPropagation()}\n\t\t\trole=\"dialog\"\n\t\t\taria-modal=\"true\"\n\t\t\taria-label={$t.translate?.bulk_replace?.title}\n\t\t>\n\t\t\t\n\t\t\t
\n\t\t\t\t

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

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

{errorMessage}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{errorMessage}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.retry}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.cancel}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.bulk_replace?.close}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\n{/if}\n\n" + "body": "\n\n\n\n\n\n\n{#if show}\n\t\n\t\t e.stopPropagation()}\n\t\t\trole=\"dialog\"\n\t\t\taria-modal=\"true\"\n\t\t\taria-label={$t.translate?.bulk_replace?.title}\n\t\t>\n\t\t\t\n\t\t\t
\n\t\t\t\t

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

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

{errorMessage}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{errorMessage}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.retry}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.cancel}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.bulk_replace?.close}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\n{/if}\n\n" }, { "contract_id": "CorrectionCell", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/CorrectionCell.svelte", "start_line": 1, - "end_line": 307, + "end_line": 326, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, + "LAYER": "UI", "PURPOSE": "Inline-editable cell for translation run results with edit-save-cancel UX and submit-to-dictionary flow.", + "TYPE": "{'view'|'editing'|'saving'|'saved'|'submitting_to_dict'|'dict_submitted'|'error'}", "UX_STATE": "error -> Error state with retry option" }, "relations": [ { "source_id": "CorrectionCell", "relation_type": "DEPENDS_ON", - "target_id": "dictionaryApi", - "target_ref": "[dictionaryApi]" + "target_id": "TranslateApi", + "target_ref": "[TranslateApi]" }, { "source_id": "CorrectionCell", @@ -87452,22 +92638,65 @@ "target_ref": "[dictionaryApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'view'|'editing'|'saving'|'saved'|'submitting_to_dict'|'dict_submitted'|'error'}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'view'|'editing'|'saving'|'saved'|'submitting_to_dict'|'dict_submitted'|'error'}" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\tlet uxState = $state('view');\n\tlet editValue = $state(value);\n\tlet displayValue = $state(value);\n\tlet errorMessage = $state('');\n\tlet dictPopupOpen = $state(false);\n\tlet dictionaries = $state([]);\n\tlet selectedDictId = $state('');\n\tlet editableContextData = $state(null);\n\tlet editableUsageNotes = $state('');\n\tlet contextEditMode = $state(false);\n\n\t$effect(() => {\n\t\t// Reset if value prop changes externally\n\t\tif (value !== displayValue) {\n\t\t\teditValue = value;\n\t\t\tdisplayValue = value;\n\t\t\tuxState = 'view';\n\t\t}\n\t});\n\n\tfunction startEditing() {\n\t\teditValue = value;\n\t\tuxState = 'editing';\n\t}\n\n\tfunction cancelEditing() {\n\t\teditValue = value;\n\t\tuxState = 'view';\n\t}\n\n\t/** @returns {Promise} */\n\tasync function saveEdit() {\n\t\tconst newValue = editValue.trim();\n\t\tif (!newValue || newValue === displayValue) {\n\t\t\tuxState = 'view';\n\t\t\treturn;\n\t\t}\n\t\tuxState = 'saving';\n\t\terrorMessage = '';\n\t\ttry {\n\t\t\tawait inlineEditCorrection(runId, recordId, languageCode, {\n\t\t\t\tfinal_value: newValue,\n\t\t\t\tsubmit_to_dictionary: false\n\t\t\t});\n\t\t\tdisplayValue = newValue;\n\t\t\tuxState = 'saved';\n\t\t\tonSave(newValue);\n\t\t\taddToast(_('translate.corrections.cell_save_success'), 'success');\n\t\t} catch (err) {\n\t\t\terrorMessage = err?.message || _('translate.corrections.cell_save_failed');\n\t\t\tuxState = 'error';\n\t\t\taddToast(errorMessage, 'error');\n\t\t}\n\t}\n\n\t/** @returns {Promise} */\n\tasync function openDictPopup() {\n\t\tdictPopupOpen = true;\n\t\teditableContextData = contextData ? JSON.parse(JSON.stringify(contextData)) : null;\n\t\teditableUsageNotes = usageNotes || '';\n\t\tcontextEditMode = false;\n\t\ttry {\n\t\t\tconst result = await dictionaryApi.fetchDictionaries({ page_size: 100 });\n\t\t\tdictionaries = Array.isArray(result) ? result : (result?.items || []);\n\t\t\tif (dictionaries.length === 1) {\n\t\t\t\tselectedDictId = dictionaries[0].id;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\taddToast(_('translate.corrections.cell_dict_load_failed'), 'error');\n\t\t}\n\t}\n\n\t/** @returns {Promise} */\n\tasync function handleDictSubmit() {\n\t\tif (!selectedDictId) {\n\t\t\taddToast(_('translate.corrections.cell_select_dict_warning'), 'warning');\n\t\t\treturn;\n\t\t}\n\t\tuxState = 'submitting_to_dict';\n\t\ttry {\n\t\t\t// First save the edit to dictionary\n\t\t\tawait inlineEditCorrection(runId, recordId, languageCode, {\n\t\t\t\tfinal_value: editValue,\n\t\t\t\tsubmit_to_dictionary: true,\n\t\t\t\tdictionary_id: selectedDictId\n\t\t\t});\n\t\t\tuxState = 'dict_submitted';\n\t\t\taddToast(_('translate.corrections.cell_dict_submit_success'), 'success');\n\t\t\tdictPopupOpen = false;\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || _('translate.corrections.cell_dict_submit_failed'), 'error');\n\t\t\tuxState = 'saved';\n\t\t}\n\t}\n\n\tfunction cancelDictSubmit() {\n\t\tdictPopupOpen = false;\n\t\tuxState = 'saved';\n\t}\n\n\tfunction handleCloseDictPopup() {\n\t\tdictPopupOpen = false;\n\t}\n\n\n\n{#if uxState === 'view'}\n\t e.key === 'Enter' && startEditing()}\n\t>\n\t\t\t{#if displayValue}\n\t\t\t{displayValue}\n\t\t{:else}\n\t\t\t\n\t\t{/if}\n\t\n\n\n{:else if uxState === 'editing'}\n\t
\n\t\t {\n\t\t\t\tif (e.key === 'Enter') saveEdit();\n\t\t\t\tif (e.key === 'Escape') cancelEditing();\n\t\t\t}}\n\t\t/>\n\t\t
\n\t\t\t\n\t\t\t\t✓\n\t\t\t\n\t\t\t\n\t\t\t\t✕\n\t\t\t\n\t\t
\n\t
\n\n\n{:else if uxState === 'saving'}\n\t
\n\t\t
\n\t\t{$t.translate?.corrections?.cell_saving}\n\t
\n\n\n{:else if uxState === 'saved'}\n\t
\n\t\t{displayValue}\n\t\t📝\n\t\t\n\t\t\t📕 {$t.translate?.corrections?.cell_submit_to_dict}\n\t\t\n\t
\n\n\n{:else if uxState === 'submitting_to_dict'}\n\t
\n\t\t
\n\t\t{$t.translate?.corrections?.cell_submitting}\n\t
\n\n\n{:else if uxState === 'dict_submitted'}\n\t
\n\t\t{displayValue}\n\t\t✅ {$t.translate?.corrections?.cell_submitted_badge}\n\t
\n\n\n{:else if uxState === 'error'}\n\t
\n\t\t{displayValue}\n\t\t{errorMessage}\n\t\t\n\t\t\t{$t.translate?.corrections?.cell_retry}\n\t\t\n\t
\n{/if}\n\n\n{#if dictPopupOpen}\n\t\n\t\t
e.stopPropagation()}>\n\t\t\t

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

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

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

\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_cancel}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_submit}\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n{/if}\n\n" + "body": "\n\n\n\n\n\n\n\n\n{#if uxState === 'view'}\n\t e.key === 'Enter' && startEditing()}\n\t>\n\t\t\t{#if displayValue}\n\t\t\t{displayValue}\n\t\t{:else}\n\t\t\t\n\t\t{/if}\n\t\n\n\n{:else if uxState === 'editing'}\n\t
\n\t\t {\n\t\t\t\tif (e.key === 'Enter') saveEdit();\n\t\t\t\tif (e.key === 'Escape') cancelEditing();\n\t\t\t}}\n\t\t/>\n\t\t
\n\t\t\t\n\t\t\t\t✓\n\t\t\t\n\t\t\t\n\t\t\t\t✕\n\t\t\t\n\t\t
\n\t
\n\n\n{:else if uxState === 'saving'}\n\t
\n\t\t
\n\t\t{$t.translate?.corrections?.cell_saving}\n\t
\n\n\n{:else if uxState === 'saved'}\n\t
\n\t\t{displayValue}\n\t\t📝\n\t\t\n\t\t\t📕 {$t.translate?.corrections?.cell_submit_to_dict}\n\t\t\n\t
\n\n\n{:else if uxState === 'submitting_to_dict'}\n\t
\n\t\t
\n\t\t{$t.translate?.corrections?.cell_submitting}\n\t
\n\n\n{:else if uxState === 'dict_submitted'}\n\t
\n\t\t{displayValue}\n\t\t✅ {$t.translate?.corrections?.cell_submitted_badge}\n\t
\n\n\n{:else if uxState === 'error'}\n\t
\n\t\t{displayValue}\n\t\t{errorMessage}\n\t\t\n\t\t\t{$t.translate?.corrections?.cell_retry}\n\t\t\n\t
\n{/if}\n\n\n{#if dictPopupOpen}\n\t\n\t\t
e.stopPropagation()}>\n\t\t\t

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

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

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

\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_cancel}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_submit}\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n{/if}\n\n" }, { "contract_id": "ScheduleConfig", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/ScheduleConfig.svelte", "start_line": 1, - "end_line": 273, + "end_line": 286, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Schedule configuration for a translation job — cron/interval/once triggers, timezone, enable/disable.", + "PURPOSE": "Component component: lib/components/translate/ScheduleConfig.svelte", + "TYPE": "{'idle'|'editing'|'enabled'|'disabled'|'no_prior_run_warning'}", "UX_FEEDBACK": "Next-3-executions preview; toast on save/error", "UX_RECOVERY": "Enable/disable toggle; delete schedule", "UX_STATE": "no_prior_run_warning -> No prior manual run, show warning" @@ -87480,22 +92709,43 @@ "target_ref": "[TranslateApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'idle'|'editing'|'enabled'|'disabled'|'no_prior_run_warning'}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'idle'|'editing'|'enabled'|'disabled'|'no_prior_run_warning'}" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\tlet uxState = $state('idle');\n\tlet cronExpression = $state('0 2 * * *');\n\tlet timezone = $state('UTC');\n\tlet executionMode = $state('full');\n\tlet isEnabled = $state(false);\n\tlet isLoading = $state(true);\n\tlet nextExecutions = $state([]);\n\tlet hasPriorRun = $state(true);\n\tlet scheduleExists = $state(false);\n\tlet isEditing = $derived(uxState === 'editing');\n\n\t$effect(() => {\n\t\tif (jobId) loadSchedule();\n\t});\n\n\tasync function loadSchedule() {\n\t\tisLoading = true;\n\t\ttry {\n\t\t\tconst sch = await fetchSchedule(jobId);\n\t\t\tif (sch && sch.cron_expression) {\n\t\t\t\tcronExpression = sch.cron_expression;\n\t\t\t\ttimezone = sch.timezone || 'UTC';\n\t\t\t\texecutionMode = sch.execution_mode || 'full';\n\t\t\t\tisEnabled = sch.is_active !== false;\n\t\t\t\tscheduleExists = true;\n\t\t\t\tuxState = isEnabled ? 'enabled' : 'disabled';\n\t\t\t\tloadNextExecutions();\n\t\t\t} else {\n\t\t\t\tuxState = 'idle';\n\t\t\t}\n\t\t} catch (err) {\n\t\t\t// 404 = no schedule\n\t\t\tuxState = 'idle';\n\t\t} finally {\n\t\t\tisLoading = false;\n\t\t}\n\t}\n\n\tasync function loadNextExecutions() {\n\t\ttry {\n\t\t\tconst result = await fetchNextExecutions(jobId, 3);\n\t\t\tnextExecutions = result?.next_executions || [];\n\t\t} catch (err) {\n\t\t\tnextExecutions = [];\n\t\t}\n\t}\n\n\tasync function handleSave() {\n\t\ttry {\n\t\t\tconst payload = {\n\t\t\t\tcron_expression: cronExpression,\n\t\t\t\ttimezone: timezone,\n\t\t\t\tis_active: isEnabled,\n\t\t\t\texecution_mode: executionMode,\n\t\t\t};\n\t\t\tawait setSchedule(jobId, payload);\n\t\t\taddToast($t.translate?.schedule?.saved || 'Schedule saved', 'success');\n\t\t\tscheduleExists = true;\n\t\t\tuxState = isEnabled ? 'enabled' : 'disabled';\n\t\t\tloadNextExecutions();\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || ($t.translate?.schedule?.save_failed || 'Failed to save schedule'), 'error');\n\t\t}\n\t}\n\n\tasync function handleDelete() {\n\t\ttry {\n\t\t\tawait deleteSchedule(jobId);\n\t\t\taddToast($t.translate?.schedule?.deleted || 'Schedule deleted', 'success');\n\t\t\tscheduleExists = false;\n\t\t\tuxState = 'idle';\n\t\t\tcronExpression = '0 2 * * *';\n\t\t\tnextExecutions = [];\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || ($t.translate?.schedule?.delete_failed || 'Failed to delete schedule'), 'error');\n\t\t}\n\t}\n\n\tasync function handleToggle() {\n\t\ttry {\n\t\t\tif (isEnabled) {\n\t\t\t\tawait disableSchedule(jobId);\n\t\t\t\tisEnabled = false;\n\t\t\t\tuxState = 'disabled';\n\t\t\t\taddToast($t.translate?.schedule?.disabled_toast || 'Schedule disabled', 'info');\n\t\t\t} else {\n\t\t\t\tif (!scheduleExists) {\n\t\t\t\t\tawait handleSave();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait enableSchedule(jobId);\n\t\t\t\tisEnabled = true;\n\t\t\t\tuxState = 'enabled';\n\t\t\t\taddToast($t.translate?.schedule?.enabled_toast || 'Schedule enabled', 'success');\n\t\t\t\tloadNextExecutions();\n\t\t\t}\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || ($t.translate?.schedule?.toggle_failed || 'Failed to toggle schedule'), 'error');\n\t\t}\n\t}\n\n\tfunction getCronDescription(cron) {\n\t\tconst parts = cron.split(' ');\n\t\tif (parts.length !== 5) return cron;\n\t\tif (parts[1] === '*' && parts[2] === '*' && parts[4] === '*') {\n\t\t\tconst hour = parts[0] === '*' ? '' : parts[0];\n\t\t\tconst min = parts[1];\n\t\t\tif (parts[0] === '*') {\n\t\t\t\treturn ($t.translate?.schedule?.description_every_hour || 'at :{minute} past the hour').replace('{minute}', min);\n\t\t\t}\n\t\t\treturn ($t.translate?.schedule?.description_daily || 'Daily at {hour}:{minute}').replace('{hour}', hour).replace('{minute}', min);\n\t\t}\n\t\treturn ($t.translate?.schedule?.description_cron || 'Cron: {cron}').replace('{cron}', cron);\n\t}\n\n\tconst commonTimezones = [\n\t\t'UTC', 'US/Eastern', 'US/Central', 'US/Mountain', 'US/Pacific',\n\t\t'Europe/London', 'Europe/Berlin', 'Europe/Moscow', 'Europe/Paris',\n\t\t'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Kolkata', 'Australia/Sydney',\n\t];\n\n\n
\n\t
\n\t\t

{$t.translate?.schedule?.title || 'Schedule Configuration'}

\n\t\t{#if !isLoading && scheduleExists}\n\t\t\t\n\t\t\t\t\n\t\t\t\t{isEnabled ? ($t.translate?.schedule?.enabled || 'Enabled') : ($t.translate?.schedule?.disabled || 'Disabled')}\n\t\t\t\n\t\t{/if}\n\t
\n\n\t{#if isLoading}\n\t\t
\n\n\t{:else if uxState === 'idle'}\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

{$t.translate?.schedule?.no_schedule || 'No schedule configured'}

\n\t\t\t

{$t.translate?.schedule?.no_schedule_hint || 'Set up a recurring schedule for automatic translations'}

\n\t\t\t\n\t\t
\n\n\t{:else if uxState === 'editing' || uxState === 'enabled' || uxState === 'disabled'}\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t\t\t{getCronDescription(cronExpression)}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each commonTimezones as tz}\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if nextExecutions.length > 0}\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.schedule?.next_executions || 'Next Executions'}

\n\t\t\t\t\t{#each nextExecutions as dt, i}\n\t\t\t\t\t\t

{i + 1}. {new Date(dt).toLocaleString()}

\n\t\t\t\t\t{/each}\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t{#if uxState === 'no_prior_run_warning'}\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t⚠ {$t.translate?.schedule?.no_prior_run_warning || 'No prior successful manual run. Schedule may run with default settings. Consider running manually first.'}\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t
\n\t\t\t\t{#if scheduleExists}\n\t\t\t\t\t\n\t\t\t\t{:else}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t\t\t{#if scheduleExists && (uxState === 'enabled' || uxState === 'disabled')}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if uxState === 'editing'}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t{/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n
\n\t
\n\t\t

{$t.translate?.schedule?.title || 'Schedule Configuration'}

\n\t\t{#if !isLoading && scheduleExists}\n\t\t\t\n\t\t\t\t\n\t\t\t\t{isEnabled ? ($t.translate?.schedule?.enabled || 'Enabled') : ($t.translate?.schedule?.disabled || 'Disabled')}\n\t\t\t\n\t\t{/if}\n\t
\n\n\t{#if isLoading}\n\t\t
\n\n\t{:else if uxState === 'idle'}\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

{$t.translate?.schedule?.no_schedule || 'No schedule configured'}

\n\t\t\t

{$t.translate?.schedule?.no_schedule_hint || 'Set up a recurring schedule for automatic translations'}

\n\t\t\t\n\t\t
\n\n\t{:else if uxState === 'editing' || uxState === 'enabled' || uxState === 'disabled'}\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t

\n\t\t\t\t\t{getCronDescription(cronExpression)}\n\t\t\t\t

\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each commonTimezones as tz}\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if nextExecutions.length > 0}\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.schedule?.next_executions || 'Next Executions'}

\n\t\t\t\t\t{#each nextExecutions as dt, i}\n\t\t\t\t\t\t

{i + 1}. {new Date(dt).toLocaleString()}

\n\t\t\t\t\t{/each}\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t{#if uxState === 'no_prior_run_warning'}\n\t\t\t\t
\n\t\t\t\t\t

\n\t\t\t\t\t\t⚠ {$t.translate?.schedule?.no_prior_run_warning || 'No prior successful manual run. Schedule may run with default settings. Consider running manually first.'}\n\t\t\t\t\t

\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t
\n\t\t\t\t{#if scheduleExists}\n\t\t\t\t\t\n\t\t\t\t{:else}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t\t\t{#if scheduleExists && (uxState === 'enabled' || uxState === 'disabled')}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if uxState === 'editing'}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t{/if}\n
\n\n" }, { "contract_id": "TermCorrectionPopup", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/TermCorrectionPopup.svelte", "start_line": 1, - "end_line": 323, + "end_line": 342, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Popup for submitting a term correction from a run result row.", + "PURPOSE": "Component component: lib/components/translate/TermCorrectionPopup.svelte", + "TYPE": "{'closed'|'selecting'|'editing'|'submitting'|'conflict_detected'|'submitted'}", "UX_FEEDBACK": "Toast on success/error; conflict dialog with action buttons", "UX_RECOVERY": "Cancel button returns to previous state", "UX_STATE": "submitted -> Success feedback" @@ -87503,9 +92753,9 @@ "relations": [ { "source_id": "TermCorrectionPopup", - "relation_type": "BINDS_TO", - "target_id": "dictionaryApi", - "target_ref": "[dictionaryApi]" + "relation_type": "DEPENDS_ON", + "target_id": "TranslateApi", + "target_ref": "[TranslateApi]" }, { "source_id": "TermCorrectionPopup", @@ -87514,22 +92764,43 @@ "target_ref": "[dictionaryApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'closed'|'selecting'|'editing'|'submitting'|'conflict_detected'|'submitted'}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'closed'|'selecting'|'editing'|'submitting'|'conflict_detected'|'submitted'}" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\tlet uxState = $state('closed');\n\tlet correctedTarget = $state('');\n\tlet dictionaries = $state([]);\n\tlet selectedDictId = $state('');\n\tlet conflictInfo = $state(null);\n\tlet submitResult = $state(null);\n\n\t// Context state\n\tlet contextData = $state(null);\n\tlet contextUsageNotes = $state('');\n\tlet hasEditedContext = $state(false);\n\tlet showContextEditor = $state(false);\n\tlet contextEditorText = $state('');\n\n\t$effect(() => {\n\t\tif (initialContextData) {\n\t\t\tcontextData = JSON.parse(JSON.stringify(initialContextData));\n\t\t\tcontextEditorText = JSON.stringify(initialContextData, null, 2);\n\t\t}\n\t\tif (initialUsageNotes) {\n\t\t\tcontextUsageNotes = initialUsageNotes;\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tif (sourceTerm && incorrectTarget) {\n\t\t\tuxState = 'selecting';\n\t\t\tloadDictionaries();\n\t\t}\n\t});\n\n\tasync function loadDictionaries() {\n\t\ttry {\n\t\t\tconst result = await dictionaryApi.fetchDictionaries({ page_size: 100 });\n\t\t\tdictionaries = Array.isArray(result) ? result : (result?.items || []);\n\t\t\tif (dictionaries.length === 1) {\n\t\t\t\tselectedDictId = dictionaries[0].id;\n\t\t\t}\n\t\t\tuxState = dictionaries.length > 0 ? 'editing' : 'selecting';\n\t\t} catch (err) {\n\t\t\taddToast(_('translate.term_correction.dict_load_failed'), 'error');\n\t\t\tuxState = 'editing';\n\t\t}\n\t}\n\n\tasync function handleSubmit() {\n\t\tif (!selectedDictId || !correctedTarget.trim()) return;\n\t\tuxState = 'submitting';\n\t\ttry {\n\t\t\tconst payload = {\n\t\t\t\tsource_term: sourceTerm,\n\t\t\t\tincorrect_target_term: incorrectTarget,\n\t\t\t\tcorrected_target_term: correctedTarget.trim(),\n\t\t\t\tdictionary_id: selectedDictId,\n\t\t\t\torigin_run_id: runId || undefined,\n\t\t\t\torigin_row_key: rowKey || undefined,\n\t\t\t};\n\t\t\tif (contextData) {\n\t\t\t\tpayload.context_data = contextData;\n\t\t\t}\n\t\t\tif (contextUsageNotes) {\n\t\t\t\tpayload.usage_notes = contextUsageNotes;\n\t\t\t}\n\t\t\tconst result = await submitCorrection(payload);\n\t\t\tif (result.action === 'conflict_detected') {\n\t\t\t\tconflictInfo = result.conflict;\n\t\t\t\tuxState = 'conflict_detected';\n\t\t\t} else if (result.action === 'created' || result.action === 'updated') {\n\t\t\t\tsubmitResult = result;\n\t\t\t\tuxState = 'submitted';\n\t\t\t\taddToast(result.message || _('translate.term_correction.submit_success'), 'success');\n\t\t\t\tonSubmitted(result);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || _('translate.term_correction.submit_failed'), 'error');\n\t\t\tuxState = 'editing';\n\t\t}\n\t}\n\n\tasync function handleConflictOverwrite() {\n\t\ttry {\n\t\t\tconst result = await submitCorrection({\n\t\t\t\tsource_term: sourceTerm,\n\t\t\t\tincorrect_target_term: incorrectTarget,\n\t\t\t\tcorrected_target_term: correctedTarget.trim(),\n\t\t\t\tdictionary_id: selectedDictId,\n\t\t\t\torigin_run_id: runId || undefined,\n\t\t\t\torigin_row_key: rowKey || undefined,\n\t\t\t});\n\t\t\tsubmitResult = result;\n\t\t\tuxState = 'submitted';\n\t\t\taddToast(result.message || _('translate.term_correction.overwrite_success'), 'success');\n\t\t\tonSubmitted(result);\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || _('translate.term_correction.overwrite_failed'), 'error');\n\t\t\tuxState = 'conflict_detected';\n\t\t}\n\t}\n\n\tfunction handleConflictKeep() {\n\t\tuxState = 'submitted';\n\t\taddToast(_('translate.term_correction.keep_success'), 'info');\n\t\tonSubmitted({ action: 'kept' });\n\t}\n\n\tfunction handleClose() {\n\t\tuxState = 'closed';\n\t\tonClose();\n\t}\n\n\n{#if uxState !== 'closed'}\n\t
\n\t\t
e.stopPropagation()}>\n\t\t\t\n\t\t\t
\n\t\t\t\t

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

\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if uxState === 'selecting'}\n\t\t\t\t

{$t.translate?.term_correction?.loading_dictionaries}

\n\n\t\t\t\n\t\t\t{:else if uxState === 'editing'}\n\t\t\t\t
\n\t\t\t\t\t{#if sourceLanguage || targetLanguage}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.term_correction?.language_pair} {sourceLanguage || $t.translate?.term_correction?.detected} → {targetLanguage || languageCode}\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t { showContextEditor = !showContextEditor; }}\n\t\t\t\t\t\t\tclass=\"w-full flex items-center justify-between px-3 py-2 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t📋\n\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.context_from_source}\n\t\t\t\t\t\t\t\t{#if contextData}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{hasEditedContext ? '✏️ ' + $t.translate?.term_correction?.edited_badge : '🤖 ' + $t.translate?.term_correction?.auto_badge}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{#if showContextEditor}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{#if contextData}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{JSON.stringify(contextData, null, 2)}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\tcontextEditorText = JSON.stringify(contextData, null, 2);\n\t\t\t\t\t\t\t\t\t\t\t\tshowContextEditor = true;\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"text-[11px] px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.edit_context}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\tcontextData = null;\n\t\t\t\t\t\t\t\t\t\t\t\thasEditedContext = true;\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"text-[11px] px-2 py-1 rounded border border-red-200 text-red-600 hover:bg-red-50\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.remove_context}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t

{$t.translate?.term_correction?.no_context}

\n\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\tcontextData = JSON.parse(JSON.stringify(initialContextData));\n\t\t\t\t\t\t\t\t\t\t\tcontextEditorText = JSON.stringify(initialContextData, null, 2);\n\t\t\t\t\t\t\t\t\t\t\thasEditedContext = false;\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tclass=\"text-[11px] px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.restore_context}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#each dictionaries as dict}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{$t.translate?.term_correction?.submit_correction}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\n\t\t\t{:else if uxState === 'submitting'}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.term_correction?.submitting}

\n\t\t\t\t
\n\n\t\t\t\n\t\t\t{:else if uxState === 'conflict_detected'}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.term_correction?.conflict_detected}

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

\n\t\t\t\t\t\t\t{$t.translate?.term_correction?.conflict_description.replace('{source}', conflictInfo?.source_term || '').replace('{existing}', conflictInfo?.existing_target_term || '')}\n\t\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\n\t\t\t{:else if uxState === 'submitted'}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t

{$t.translate?.term_correction?.submitted}

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

{submitResult?.message || ''}

\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t{/if}\n\t\t
\n\t
\n{/if}\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n{#if uxState !== 'closed'}\n\t
\n\t\t
e.stopPropagation()}>\n\t\t\t\n\t\t\t
\n\t\t\t\t

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

\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if uxState === 'selecting'}\n\t\t\t\t

{$t.translate?.term_correction?.loading_dictionaries}

\n\n\t\t\t\n\t\t\t{:else if uxState === 'editing'}\n\t\t\t\t
\n\t\t\t\t\t{#if sourceLanguage || targetLanguage}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.term_correction?.language_pair} {sourceLanguage || $t.translate?.term_correction?.detected} → {targetLanguage || languageCode}\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t { showContextEditor = !showContextEditor; }}\n\t\t\t\t\t\t\tclass=\"w-full flex items-center justify-between px-3 py-2 text-xs font-medium text-gray-600 bg-gray-50 hover:bg-gray-100 transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t📋\n\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.context_from_source}\n\t\t\t\t\t\t\t\t{#if contextData}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{hasEditedContext ? '✏️ ' + $t.translate?.term_correction?.edited_badge : '🤖 ' + $t.translate?.term_correction?.auto_badge}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{#if showContextEditor}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{#if contextData}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{JSON.stringify(contextData, null, 2)}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\tcontextEditorText = JSON.stringify(contextData, null, 2);\n\t\t\t\t\t\t\t\t\t\t\t\tshowContextEditor = true;\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"text-[11px] px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.edit_context}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\tcontextData = null;\n\t\t\t\t\t\t\t\t\t\t\t\thasEditedContext = true;\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"text-[11px] px-2 py-1 rounded border border-destructive-light text-red-600 hover:bg-red-50\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.remove_context}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t

{$t.translate?.term_correction?.no_context}

\n\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\tcontextData = JSON.parse(JSON.stringify(initialContextData));\n\t\t\t\t\t\t\t\t\t\t\tcontextEditorText = JSON.stringify(initialContextData, null, 2);\n\t\t\t\t\t\t\t\t\t\t\thasEditedContext = false;\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tclass=\"text-[11px] px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{$t.translate?.term_correction?.restore_context}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#each dictionaries as dict}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{$t.translate?.term_correction?.submit_correction}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\n\t\t\t{:else if uxState === 'submitting'}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.term_correction?.submitting}

\n\t\t\t\t
\n\n\t\t\t\n\t\t\t{:else if uxState === 'conflict_detected'}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.term_correction?.conflict_detected}

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

\n\t\t\t\t\t\t\t{$t.translate?.term_correction?.conflict_description.replace('{source}', conflictInfo?.source_term || '').replace('{existing}', conflictInfo?.existing_target_term || '')}\n\t\t\t\t\t\t

\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\n\t\t\t{:else if uxState === 'submitted'}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t

{$t.translate?.term_correction?.submitted}

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

{submitResult?.message || ''}

\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t{/if}\n\t\t
\n\t
\n{/if}\n\n" }, { "contract_id": "TranslationMetricsDashboard", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/TranslationMetricsDashboard.svelte", "start_line": 1, - "end_line": 198, + "end_line": 184, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Admin metrics dashboard for translation — per-job run counts, success/failure ratio,", + "PURPOSE": "Component component: lib/components/translate/TranslationMetricsDashboard.svelte", + "TYPE": "{'loading'|'loaded'|'error'|'empty'}", "UX_FEEDBACK": "Spinner during load; card grid for totals; table for per-job breakdown", "UX_RECOVERY": "Retry button on error", "UX_STATE": "empty -> No metrics data available" @@ -87542,24 +92813,43 @@ "target_ref": "[TranslateApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'loading'|'loaded'|'error'|'empty'}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'loading'|'loaded'|'error'|'empty'}" + } + } + ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n
\n

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

\n

{$t.translate?.metrics?.subtitle}

\n
\n \n \n \n \n {$t.translate?.metrics?.refresh}\n \n
\n\n \n {#if uxState === 'loading'}\n
\n {#each Array(3) as _}\n
\n {/each}\n
\n\n \n {:else if uxState === 'error'}\n
\n

{errorMessage}

\n \n {$t.translate?.common?.retry}\n \n
\n\n \n {:else if uxState === 'empty'}\n
\n \n \n \n

{$t.translate?.metrics?.no_data}

\n
\n\n \n {:else if uxState === 'loaded'}\n \n
\n
\n

{$t.translate?.metrics?.total_runs}

\n

{totalRuns}

\n
\n
\n

{$t.translate?.metrics?.success_rate}

\n

{successRate}%

\n
\n
\n

{$t.translate?.metrics?.failed_runs}

\n

{failedRuns}

\n
\n
\n

{$t.translate?.metrics?.total_records}

\n

{totalRecords.toLocaleString()}

\n
\n
\n

{$t.translate?.metrics?.total_tokens}

\n

{totalTokens.toLocaleString()}

\n
\n
\n

{$t.translate?.metrics?.total_cost}

\n

${totalCost.toFixed(2)}

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

{$t.translate?.metrics?.runs_per_job}

\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n {#each metrics as m}\n \n \n \n \n \n \n \n \n {/each}\n \n
{$t.translate?.metrics?.job}{$t.translate?.metrics?.runs}{$t.translate?.metrics?.successful}{$t.translate?.metrics?.failed}{$t.translate?.metrics?.tokens}{$t.translate?.metrics?.cost}
{m.job_name || m.job_id}{m.total_runs || 0}{m.successful_runs || 0}{m.failed_runs || 0}{(m.total_tokens || 0).toLocaleString()}${(m.total_cost || 0).toFixed(2)}
\n
\n
\n\n \n {#if avgLatency !== null}\n
\n

{$t.translate?.metrics?.avg_latency}

\n

{avgLatency}s

\n
\n {/if}\n {/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n
\n

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

\n

{$t.translate?.metrics?.subtitle}

\n
\n \n \n \n \n {$t.translate?.metrics?.refresh}\n \n
\n\n \n {#if uxState === 'loading'}\n
\n {#each Array(3) as _}\n
\n {/each}\n
\n\n \n {:else if uxState === 'error'}\n
\n

{errorMessage}

\n \n {$t.translate?.common?.retry}\n \n
\n\n \n {:else if uxState === 'empty'}\n
\n \n \n \n

{$t.translate?.metrics?.no_data}

\n
\n\n \n {:else if uxState === 'loaded'}\n \n
\n
\n

{$t.translate?.metrics?.total_runs}

\n

{totalRuns}

\n
\n
\n

{$t.translate?.metrics?.success_rate}

\n

{successRate}%

\n
\n
\n

{$t.translate?.metrics?.failed_runs}

\n

{failedRuns}

\n
\n
\n

{$t.translate?.metrics?.total_records}

\n

{totalRecords.toLocaleString()}

\n
\n
\n

{$t.translate?.metrics?.total_tokens}

\n

{totalTokens.toLocaleString()}

\n
\n
\n

{$t.translate?.metrics?.total_cost}

\n

${totalCost.toFixed(2)}

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

{$t.translate?.metrics?.runs_per_job}

\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n {#each metrics as m}\n \n \n \n \n \n \n \n \n {/each}\n \n
{$t.translate?.metrics?.job}{$t.translate?.metrics?.runs}{$t.translate?.metrics?.successful}{$t.translate?.metrics?.failed}{$t.translate?.metrics?.tokens}{$t.translate?.metrics?.cost}
{m.job_name || m.job_id}{m.total_runs || 0}{m.successful_runs || 0}{m.failed_runs || 0}{(m.total_tokens || 0).toLocaleString()}${(m.total_cost || 0).toFixed(2)}
\n
\n
\n\n \n {#if avgLatency !== null}\n
\n

{$t.translate?.metrics?.avg_latency}

\n

{avgLatency}s

\n
\n {/if}\n {/if}\n
\n\n" }, { "contract_id": "TranslationPreview", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/TranslationPreview.svelte", "start_line": 1, - "end_line": 549, + "end_line": 561, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "POST": "User can preview, approve/edit/reject per-language rows, and accept the session to gate full execution.", - "PRE": "jobId is a valid translation job ID.", - "PURPOSE": "Multi-language translation preview with dynamic per-language columns, approve/edit/reject, cost estimate, and accept gate.", + "PURPOSE": "Component component: lib/components/translate/TranslationPreview.svelte", + "TYPE": "{'idle'|'loading'|'preview_loaded'|'preview_error'|'accepted'|'stale_config'}", "UX_FEEDBACK": "spinner during preview; visual distinction for LLM vs edited translations; cost warning for large previews.", "UX_RECOVERY": "retry preview; re-fetch with updated config.", "UX_STATE": "stale_config -> Configuration changed since preview was generated" @@ -87568,8 +92858,8 @@ { "source_id": "TranslationPreview", "relation_type": "DEPENDS_ON", - "target_id": "api_module", - "target_ref": "[api_module]" + "target_id": "TranslateApi", + "target_ref": "[TranslateApi]" }, { "source_id": "TranslationPreview", @@ -87580,34 +92870,35 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Component' at C3", + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'idle'|'loading'|'preview_loaded'|'preview_error'|'accepted'|'stale_config'}' is not in allowed enum", "detail": { - "actual_complexity": 3, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Component' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Component" + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'idle'|'loading'|'preview_loaded'|'preview_error'|'accepted'|'stale_config'}" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\tlet uxState = $state('idle');\n\tlet session = $state(null);\n\tlet records = $state([]);\n\tlet targetLanguages = $state([]);\n\tlet costEstimate = $state(null);\n\tlet errorMessage = $state('');\n\tlet sampleSize = $state(10);\n\tlet isAccepting = $state(false);\n\n\t// Per-language editing state: { `${rowId}_${langCode}`: true }\n\tlet editingCells = $state({});\n\tlet editValues = $state({});\n\n\t// Derived\n\tlet approvedCount = $derived(records.filter(r => r.status === 'APPROVED').length);\n\tlet rejectedCount = $derived(records.filter(r => r.status === 'REJECTED').length);\n\tlet pendingCount = $derived(records.filter(r => r.status === 'PENDING').length);\n\tlet isAllApproved = $derived(pendingCount === 0 && records.length > 0);\n\n\tfunction getRowStatusLabel(status) {\n\t\tconst labels = {\n\t\t\tAPPROVED: $t.translate?.preview?.status_approved || 'Approved',\n\t\t\tREJECTED: $t.translate?.preview?.status_rejected || 'Rejected',\n\t\t\tPENDING: $t.translate?.preview?.status_pending || 'Pending',\n\t\t};\n\t\treturn labels[status] || status;\n\t}\n\n\t/** Get language status for a record and language code */\n\tfunction getLangStatus(record, langCode) {\n\t\tconst lang = (record.languages || []).find(l => l.language_code === langCode);\n\t\treturn lang ? lang.status : 'pending';\n\t}\n\n\t/** Get translated value for a record and language code */\n\tfunction getLangValue(record, langCode) {\n\t\tconst lang = (record.languages || []).find(l => l.language_code === langCode);\n\t\treturn lang ? (lang.user_edit || lang.final_value || lang.translated_value || '') : '';\n\t}\n\n\t/** Get detected language for a record */\n\tfunction getDetectedLang(record) {\n\t\tif (record.source_language_detected && record.source_language_detected !== 'und') {\n\t\t\treturn record.source_language_detected;\n\t\t}\n\t\tconst langs = record.languages || [];\n\t\tfor (const l of langs) {\n\t\t\tif (l.source_language_detected && l.source_language_detected !== 'und') {\n\t\t\t\treturn l.source_language_detected;\n\t\t\t}\n\t\t}\n\t\treturn 'und';\n\t}\n\n\t/** @returns {Promise} */\n\tasync function handlePreview() {\n\t\tuxState = 'loading';\n\t\terrorMessage = '';\n\t\ttry {\n\t\t\tconst result = await fetchPreview(jobId, sampleSize, environmentId);\n\t\t\tsession = result;\n\t\t\trecords = (result.records || []).map(r => ({ ...r, _isEdited: false }));\n\t\t\ttargetLanguages = result.target_languages || [];\n\t\t\tcostEstimate = result.cost_estimate || null;\n\t\t\tuxState = 'preview_loaded';\n\t\t\taddToast($t.translate?.preview?.preview_loaded?.replace('{count}', records.length), 'success');\n\t\t} catch (err) {\n\t\t\terrorMessage = err?.message || $t.translate?.preview?.preview_failed;\n\t\t\tuxState = 'preview_error';\n\t\t\taddToast(errorMessage, 'error');\n\t\t}\n\t}\n\n\t/** @param {string} rowId */\n\tasync function handleApprove(rowId) {\n\t\ttry {\n\t\t\tconst updated = await approveRow(jobId, rowId);\n\t\t\trecords = records.map(r => r.id === rowId ? {\n\t\t\t\t...r,\n\t\t\t\tstatus: updated.status || 'APPROVED',\n\t\t\t\ttarget_sql: updated.target_sql || r.target_sql,\n\t\t\t\tlanguages: updated.languages || r.languages,\n\t\t\t\t_isEdited: false\n\t\t\t} : r);\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.preview?.approve_failed, 'error');\n\t\t}\n\t}\n\n\t/** @param {string} rowId */\n\tasync function handleReject(rowId) {\n\t\ttry {\n\t\t\tconst updated = await rejectRow(jobId, rowId);\n\t\t\trecords = records.map(r => r.id === rowId ? {\n\t\t\t\t...r,\n\t\t\t\tstatus: updated.status || 'REJECTED',\n\t\t\t\tlanguages: updated.languages || r.languages,\n\t\t\t\t_isEdited: false\n\t\t\t} : r);\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.preview?.reject_failed, 'error');\n\t\t}\n\t}\n\n\t/** @param {string} rowId @param {string} langCode */\n\tasync function handleApproveLanguage(rowId, langCode) {\n\t\ttry {\n\t\t\tconst updated = await approveRow(jobId, rowId, langCode);\n\t\t\trecords = records.map(r => r.id === rowId ? {\n\t\t\t\t...r,\n\t\t\t\tstatus: updated.status || r.status,\n\t\t\t\ttarget_sql: updated.target_sql || r.target_sql,\n\t\t\t\tlanguages: updated.languages || r.languages,\n\t\t\t\t_isEdited: false\n\t\t\t} : r);\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.preview?.approve_failed, 'error');\n\t\t}\n\t}\n\n\t/** @param {string} rowId @param {string} langCode */\n\tasync function handleRejectLanguage(rowId, langCode) {\n\t\ttry {\n\t\t\tconst updated = await rejectRow(jobId, rowId, langCode);\n\t\t\trecords = records.map(r => r.id === rowId ? {\n\t\t\t\t...r,\n\t\t\t\tstatus: updated.status || r.status,\n\t\t\t\tlanguages: updated.languages || r.languages,\n\t\t\t\t_isEdited: false\n\t\t\t} : r);\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.preview?.reject_failed, 'error');\n\t\t}\n\t}\n\n\t/** @param {string} rowId @param {string} langCode */\n\tfunction startLangEdit(rowId, langCode) {\n\t\tconst key = `${rowId}_${langCode}`;\n\t\teditingCells = { ...editingCells, [key]: true };\n\t\teditValues = { ...editValues, [key]: getLangValue(records.find(r => r.id === rowId), langCode) };\n\t}\n\n\t/** @param {string} rowId @param {string} langCode */\n\tasync function saveLangEdit(rowId, langCode) {\n\t\tconst key = `${rowId}_${langCode}`;\n\t\tconst value = (editValues[key] || '').trim();\n\t\tif (!value) return;\n\t\ttry {\n\t\t\tconst updated = await editRow(jobId, rowId, value, langCode);\n\t\t\trecords = records.map(r =>\n\t\t\t\tr.id === rowId\n\t\t\t\t\t? {\n\t\t\t\t\t\t...r,\n\t\t\t\t\t\tstatus: updated.status || r.status,\n\t\t\t\t\t\ttarget_sql: updated.target_sql || r.target_sql,\n\t\t\t\t\t\tlanguages: updated.languages || r.languages,\n\t\t\t\t\t\t_isEdited: true\n\t\t\t\t\t}\n\t\t\t\t\t: r\n\t\t\t);\n\t\t\tconst newEditing = { ...editingCells };\n\t\t\tdelete newEditing[key];\n\t\t\teditingCells = newEditing;\n\t\t\tconst newValues = { ...editValues };\n\t\t\tdelete newValues[key];\n\t\t\teditValues = newValues;\n\t\t\taddToast($t.translate?.preview?.row_updated, 'success');\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.preview?.edit_failed, 'error');\n\t\t}\n\t}\n\n\t/** @param {string} rowId @param {string} langCode */\n\tfunction cancelLangEdit(rowId, langCode) {\n\t\tconst key = `${rowId}_${langCode}`;\n\t\tconst newEditing = { ...editingCells };\n\t\tdelete newEditing[key];\n\t\teditingCells = newEditing;\n\t\tconst newValues = { ...editValues };\n\t\tdelete newValues[key];\n\t\teditValues = newValues;\n\t}\n\n\tasync function handleBulkApprove() {\n\t\tfor (const row of records) {\n\t\t\tif (row.status === 'PENDING') {\n\t\t\t\tawait handleApprove(row.id);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function handleAccept() {\n\t\tisAccepting = true;\n\t\ttry {\n\t\t\tconst result = await acceptPreview(jobId);\n\t\t\tuxState = 'accepted';\n\t\t\tonAccept();\n\t\t\taddToast($t.translate?.preview?.accepted_body, 'success');\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.preview?.accept_failed, 'error');\n\t\t} finally {\n\t\t\tisAccepting = false;\n\t\t}\n\t}\n\n\n
\n\t\n\t
\n\t\t

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

\n\t\t{#if uxState === 'preview_loaded'}\n\t\t\t\n\t\t\t\t{session?.status === 'APPLIED' ? $t.translate?.preview?.accepted_badge : $t.translate?.preview?.active_badge}\n\t\t\t\n\t\t{/if}\n\t
\n\n\t\n\t{#if uxState === 'idle'}\n\t\t
\n\t\t\t

\n\t\t\t\t{$t.translate?.preview?.title} — {$t.translate?.preview?.sample_size} {sampleSize}\n\t\t\t

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t{sampleSize}\n\t\t\t
\n\t\t\t{#if sampleSize > 30}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{($t.translate?.preview?.cost_warning) || 'Large sample size may incur higher costs'}\n\t\t\t\t
\n\t\t\t{/if}\n\t\t\t\n\t\t\t\t{$t.translate?.preview?.run_preview}\n\t\t\t\n\t\t
\n\n\t\n\t{:else if uxState === 'loading'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t{$t.translate?.preview?.running_preview}\n\t\t\t
\n\t\t\t
\n\t\t\t\t{#each Array(3) as _}\n\t\t\t\t\t
\n\t\t\t\t{/each}\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'preview_error'}\n\t\t
\n\t\t\t

{errorMessage}

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.preview?.retry_preview}\n\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'preview_loaded'}\n\t\t\n\t\t{#if costEstimate}\n\t\t\t
\n\t\t\t\t

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

\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.sample_rows}\n\t\t\t\t\t\t{costEstimate.sample_size}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.sample_tokens}\n\t\t\t\t\t\t{costEstimate.sample_total_tokens?.toLocaleString()}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.sample_cost}\n\t\t\t\t\t\t${costEstimate.sample_cost?.toFixed(6)}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.est_total_cost}\n\t\t\t\t\t\t${costEstimate.estimated_cost?.toFixed(6)}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t{$t.translate?.preview?.across_languages?.replace('{count}', costEstimate.num_languages || targetLanguages.length || 1)}\n\t\t\t\t

\n\t\t\t\t\n\t\t\t\t{#if costEstimate.warning}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{costEstimate.warning}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t
\n\t\t{/if}\n\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t{$t.translate?.preview?.rows_count?.replace('{count}', records.length)}\n\t\t\t\t{$t.translate?.preview?.approved_count?.replace('{count}', approvedCount)}\n\t\t\t\t{$t.translate?.preview?.rejected_count?.replace('{count}', rejectedCount)}\n\t\t\t\t{#if pendingCount > 0}\n\t\t\t\t\t{$t.translate?.preview?.pending_count?.replace('{count}', pendingCount)}\n\t\t\t\t{/if}\n\t\t\t\t{#if targetLanguages.length > 0}\n\t\t\t\t\t{$t.translate?.preview?.languages_count?.replace('{count}', targetLanguages.length)}\n\t\t\t\t{/if}\n\t\t\t
\n\t\t\t
\n\t\t\t\t{#if pendingCount > 0}\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.preview?.approve_all}\n\t\t\t\t\t\n\t\t\t\t{/if}\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.preview?.re_run}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\n\t\t\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each records as row, idx}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t{@const langKey = `${row.id}_${lang}`}\n\t\t\t\t\t\t\t\t{@const langVal = getLangValue(row, lang)}\n\t\t\t\t\t\t\t\t{@const langStatus = getLangStatus(row, lang)}\n\t\t\t\t\t\t\t\t{@const isEditing = editingCells[langKey]}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t
#{$t.translate?.preview?.table_source}{$t.translate?.preview?.detected_language}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{$t.translate?.preview?.table_status}
{idx + 1}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{row.source_sql || $t.translate?.preview?.empty_placeholder}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{getDetectedLang(row)}\n\t\t\t\t\t\t\t\t{#if getDetectedLang(row) === 'und'}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#if isEditing}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t saveLangEdit(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-700\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.save_edit}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t cancelLangEdit(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-xs border border-gray-300 text-gray-600 rounded hover:bg-gray-50\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.cancel_edit}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{langVal || $t.translate?.preview?.pending_placeholder}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t{#if langStatus !== 'approved' && langStatus !== 'edited'}\n\t\t\t\t\t\t\t\t\t\t\t\t handleApproveLanguage(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.approve}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.approve}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t startLangEdit(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.edit}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.edit}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#if langStatus !== 'rejected'}\n\t\t\t\t\t\t\t\t\t\t\t\t handleRejectLanguage(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{getRowStatusLabel(row.status)}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{#if row.status !== 'APPROVED'}\n\t\t\t\t\t\t\t\t\t\t\t handleApprove(row.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.approve_all}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.approve}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t{#if row.status !== 'REJECTED'}\n\t\t\t\t\t\t\t\t\t\t\t handleReject(row.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t
\n\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t

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

\n\t\t\t\t

\n\t\t\t\t\t{isAllApproved ? $t.translate?.preview?.quality_gate_ready : $t.translate?.preview?.quality_gate_pending}\n\t\t\t\t

\n\t\t\t
\n\t\t\t\n\t\t\t\t{isAccepting ? $t.translate?.preview?.accepting : $t.translate?.preview?.accept_preview}\n\t\t\t\n\t\t
\n\n\t\n\t{:else if uxState === 'accepted'}\n\t\t
\n\t\t\t
\n\t\t\t

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

\n\t\t\t

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

\n\t\t\t\n\t\t\t\t{$t.translate?.preview?.run_preview_again}\n\t\t\t\n\t\t
\n\t{/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\t\n\t
\n\t\t

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

\n\t\t{#if uxState === 'preview_loaded'}\n\t\t\t\n\t\t\t\t{session?.status === 'APPLIED' ? $t.translate?.preview?.accepted_badge : $t.translate?.preview?.active_badge}\n\t\t\t\n\t\t{/if}\n\t
\n\n\t\n\t{#if uxState === 'idle'}\n\t\t
\n\t\t\t

\n\t\t\t\t{$t.translate?.preview?.title} — {$t.translate?.preview?.sample_size} {sampleSize}\n\t\t\t

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t{sampleSize}\n\t\t\t
\n\t\t\t{#if sampleSize > 30}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{($t.translate?.preview?.cost_warning) || 'Large sample size may incur higher costs'}\n\t\t\t\t
\n\t\t\t{/if}\n\t\t\t\n\t\t\t\t{$t.translate?.preview?.run_preview}\n\t\t\t\n\t\t
\n\n\t\n\t{:else if uxState === 'loading'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t{$t.translate?.preview?.running_preview}\n\t\t\t
\n\t\t\t
\n\t\t\t\t{#each Array(3) as _}\n\t\t\t\t\t
\n\t\t\t\t{/each}\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'preview_error'}\n\t\t
\n\t\t\t

{errorMessage}

\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.preview?.retry_preview}\n\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'preview_loaded'}\n\t\t\n\t\t{#if costEstimate}\n\t\t\t
\n\t\t\t\t

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

\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.sample_rows}\n\t\t\t\t\t\t{costEstimate.sample_size}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.sample_tokens}\n\t\t\t\t\t\t{costEstimate.sample_total_tokens?.toLocaleString()}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.sample_cost}\n\t\t\t\t\t\t${costEstimate.sample_cost?.toFixed(6)}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.preview?.est_total_cost}\n\t\t\t\t\t\t${costEstimate.estimated_cost?.toFixed(6)}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t

\n\t\t\t\t\t{$t.translate?.preview?.across_languages?.replace('{count}', costEstimate.num_languages || targetLanguages.length || 1)}\n\t\t\t\t

\n\t\t\t\t\n\t\t\t\t{#if costEstimate.warning}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{costEstimate.warning}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t
\n\t\t{/if}\n\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t{$t.translate?.preview?.rows_count?.replace('{count}', records.length)}\n\t\t\t\t{$t.translate?.preview?.approved_count?.replace('{count}', approvedCount)}\n\t\t\t\t{$t.translate?.preview?.rejected_count?.replace('{count}', rejectedCount)}\n\t\t\t\t{#if pendingCount > 0}\n\t\t\t\t\t{$t.translate?.preview?.pending_count?.replace('{count}', pendingCount)}\n\t\t\t\t{/if}\n\t\t\t\t{#if targetLanguages.length > 0}\n\t\t\t\t\t{$t.translate?.preview?.languages_count?.replace('{count}', targetLanguages.length)}\n\t\t\t\t{/if}\n\t\t\t
\n\t\t\t
\n\t\t\t\t{#if pendingCount > 0}\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.preview?.approve_all}\n\t\t\t\t\t\n\t\t\t\t{/if}\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.preview?.re_run}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\n\t\t\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t{#each records as row, idx}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t{@const langKey = `${row.id}_${lang}`}\n\t\t\t\t\t\t\t\t{@const langVal = getLangValue(row, lang)}\n\t\t\t\t\t\t\t\t{@const langStatus = getLangStatus(row, lang)}\n\t\t\t\t\t\t\t\t{@const isEditing = editingCells[langKey]}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t{/each}\n\t\t\t\t\n\t\t\t
#{$t.translate?.preview?.table_source}{$t.translate?.preview?.detected_language}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{$t.translate?.preview?.table_status}
{idx + 1}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{row.source_sql || $t.translate?.preview?.empty_placeholder}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{getDetectedLang(row)}\n\t\t\t\t\t\t\t\t{#if getDetectedLang(row) === 'und'}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#if isEditing}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t saveLangEdit(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-xs bg-primary text-white rounded hover:bg-primary-hover\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.save_edit}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t cancelLangEdit(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-xs border border-gray-300 text-gray-600 rounded hover:bg-gray-50\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.cancel_edit}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{langVal || $t.translate?.preview?.pending_placeholder}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t{#if langStatus !== 'approved' && langStatus !== 'edited'}\n\t\t\t\t\t\t\t\t\t\t\t\t handleApproveLanguage(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.approve}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.approve}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t startLangEdit(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.edit}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.edit}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#if langStatus !== 'rejected'}\n\t\t\t\t\t\t\t\t\t\t\t\t handleRejectLanguage(row.id, lang)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{getRowStatusLabel(row.status)}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{#if row.status !== 'APPROVED'}\n\t\t\t\t\t\t\t\t\t\t\t handleApprove(row.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.approve_all}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.approve}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t{#if row.status !== 'REJECTED'}\n\t\t\t\t\t\t\t\t\t\t\t handleReject(row.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-1.5 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.preview?.reject}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t
\n\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t

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

\n\t\t\t\t

\n\t\t\t\t\t{isAllApproved ? $t.translate?.preview?.quality_gate_ready : $t.translate?.preview?.quality_gate_pending}\n\t\t\t\t

\n\t\t\t
\n\t\t\t\n\t\t\t\t{isAccepting ? $t.translate?.preview?.accepting : $t.translate?.preview?.accept_preview}\n\t\t\t\n\t\t
\n\n\t\n\t{:else if uxState === 'accepted'}\n\t\t
\n\t\t\t
\n\t\t\t

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

\n\t\t\t

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

\n\t\t\t\n\t\t\t\t{$t.translate?.preview?.run_preview_again}\n\t\t\t\n\t\t
\n\t{/if}\n
\n\n" }, { "contract_id": "TranslationRunGlobalIndicator", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte", "start_line": 1, - "end_line": 203, + "end_line": 207, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -87627,21 +92918,20 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n{#if show}\n\t\n\t\t{#if isActive}\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{label}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.common?.open || 'Open'}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{isCancelling\n\t\t\t\t\t\t\t\t\t? ($t.translate?.run?.cancelling || 'Cancelling...')\n\t\t\t\t\t\t\t\t\t: ($t.translate?.run?.cancel || 'Cancel')}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.total || 'Total')}\n\t\t\t\t\t\t\t{totalRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.success || 'OK')}\n\t\t\t\t\t\t\t{successfulRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.failed || 'Fail')}\n\t\t\t\t\t\t\t{failedRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.skipped || 'Skip')}\n\t\t\t\t\t\t\t{skippedRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t{:else}\n\t\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{#if uxState === 'completed'}\n\t\t\t\t\t\t\n\t\t\t\t\t{:else if uxState === 'partial' || uxState === 'failed'}\n\t\t\t\t\t\t\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{label}\n\t\t\t\t\t{#if uxState === 'partial'}\n\t\t\t\t\t\t{successfulRecords}/{totalRecords}\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t▶ {$t.common?.open || 'Open'}\n\t\t\t\t\n\t\t\t
\n\t\t{/if}\n\t
\n{/if}\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n{#if show}\n\t\n\t\t{#if isActive}\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{label}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.common?.open || 'Open'}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{isCancelling\n\t\t\t\t\t\t\t\t\t? ($t.translate?.run?.cancelling || 'Cancelling...')\n\t\t\t\t\t\t\t\t\t: ($t.translate?.run?.cancel || 'Cancel')}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.total || 'Total')}\n\t\t\t\t\t\t\t{totalRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.success || 'OK')}\n\t\t\t\t\t\t\t{successfulRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.failed || 'Fail')}\n\t\t\t\t\t\t\t{failedRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{($t.translate?.run?.skipped || 'Skip')}\n\t\t\t\t\t\t\t{skippedRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t{:else}\n\t\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{#if uxState === 'completed'}\n\t\t\t\t\t\t\n\t\t\t\t\t{:else if uxState === 'partial' || uxState === 'failed'}\n\t\t\t\t\t\t\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{label}\n\t\t\t\t\t{#if uxState === 'partial'}\n\t\t\t\t\t\t{successfulRecords}/{totalRecords}\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t▶ {$t.common?.open || 'Open'}\n\t\t\t\t\n\t\t\t
\n\t\t{/if}\n\t
\n{/if}\n\n" }, { "contract_id": "TranslationRunProgress", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/TranslationRunProgress.svelte", "start_line": 1, - "end_line": 309, + "end_line": 228, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "POST": "Real-time progress display with two-phase (translation + insert) and state transitions.", - "PRE": "runId is a valid translation run ID.", - "PURPOSE": "WebSocket-driven progress bar with batch counter, success/failure/skip counts, two-phase display, and cancel button.", + "PURPOSE": "Component component: lib/components/translate/TranslationRunProgress.svelte", + "TYPE": "{{ runId: string, onRetry?: () => void, onRetryInsert?: () => void, onComplete?: (statusData: any) => void }}", "UX_FEEDBACK": "progress bar with percentage; batch counter; per-phase counts; cancel button during running/inserting.", "UX_RECOVERY": "retry on failure states.", "UX_STATE": "cancelled -> Run was cancelled" @@ -87650,8 +92940,8 @@ { "source_id": "TranslationRunProgress", "relation_type": "DEPENDS_ON", - "target_id": "api_module", - "target_ref": "[api_module]" + "target_id": "TranslateApi", + "target_ref": "[TranslateApi]" }, { "source_id": "TranslationRunProgress", @@ -87662,42 +92952,40 @@ ], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Component' at C3", + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{{ runId: string, onRetry?: () => void, onRetryInsert?: () => void, onComplete?: (statusData: any) => void }}' is not in allowed enum", "detail": { - "actual_complexity": 3, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Component' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Component" + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{{ runId: string, onRetry?: () => void, onRetryInsert?: () => void, onComplete?: (statusData: any) => void }}" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t */\n\tlet uxState = $state('idle');\n\tlet status = $state(null);\n\tlet pollingInterval = $state(null);\n\tlet isCancelling = $state(false);\n\tlet pollingStarted = $state(false);\n\t// Safety net: stop polling after 300 attempts (10 min at 2s interval)\n\t// to prevent infinite spinner if the run gets stuck.\n\tlet pollCount = $state(0);\n\tconst MAX_POLLS = 300;\n\n\t// Derived\n\tlet totalRecords = $derived(status?.total_records || 0);\n\tlet successfulRecords = $derived(status?.successful_records || 0);\n\tlet failedRecords = $derived(status?.failed_records || 0);\n\tlet skippedRecords = $derived(status?.skipped_records || 0);\n\tlet progressPct = $derived.by(() => {\n\t\tif (totalRecords === 0) return 0;\n\t\treturn Math.round(((successfulRecords + failedRecords + skippedRecords) / totalRecords) * 100);\n\t});\n\tlet batchCount = $derived(status?.batch_count || 0);\n\tlet insertStatus = $derived(status?.insert_status || null);\n\n\tlet isRunning = $derived(uxState === 'running' || uxState === 'inserting');\n\n\t// Use onMount instead of $effect because $effect cleanup can fire\n\t// unexpectedly during re-renders in Svelte 5, stopping the polling\n\t// interval while leaving uxState as 'running' — the spinner keeps\n\t// spinning forever with no further polls. onMount/onDestroy gives\n\t// deterministic lifecycle: start when the component appears, stop\n\t// only when it is destroyed (currentRunId → null).\n\tonMount(() => {\n\t\tif (runId) {\n\t\t\tpollingStarted = true;\n\t\t\tstartPolling();\n\t\t}\n\t\treturn () => {\n\t\t\tstopPolling();\n\t\t};\n\t});\n\n\tfunction startPolling() {\n\t\tstopPolling();\n\t\tpollingInterval = setInterval(pollStatus, 2000);\n\t\tpollStatus();\n\t}\n\n\tfunction stopPolling() {\n\t\tif (pollingInterval) {\n\t\t\tclearInterval(pollingInterval);\n\t\t\tpollingInterval = null;\n\t\t}\n\t}\n\n\t/** @returns {Promise} */\n\tasync function pollStatus() {\n\t\t// Safety net: stop polling after MAX_POLLS attempts\n\t\tpollCount++;\n\t\tif (pollCount > MAX_POLLS) {\n\t\t\tconsole.warn('[TranslationRunProgress] Max polls reached, stopping');\n\t\t\tstopPolling();\n\t\t\tuxState = 'failed';\n\t\t\tonComplete({ status: 'TIMEOUT', error_message: 'Translation run timed out' });\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst data = await fetchRunStatus(runId);\n\t\t\tstatus = data;\n\n\t\t\t// Determine state from status\n\t\t\tconst s = data?.status;\n\t\t\tconst insertS = data?.insert_status;\n\n\t\t\tif (s === 'PENDING' || s === 'RUNNING') {\n\t\t\t\tif (insertS === 'started' || insertS === 'pending' || insertS === 'running') {\n\t\t\t\t\tuxState = 'inserting';\n\t\t\t\t} else {\n\t\t\t\t\tuxState = 'running';\n\t\t\t\t}\n\t\t\t} else if (s === 'COMPLETED') {\n\t\t\t\tif (insertS === 'failed' || insertS === 'timeout') {\n\t\t\t\t\tuxState = 'insert_failed';\n\t\t\t\t} else if (data.failed_records > 0) {\n\t\t\t\t\tuxState = 'partial';\n\t\t\t\t} else {\n\t\t\t\t\tuxState = 'completed';\n\t\t\t\t}\n\t\t\t\tstopPolling();\n\t\t\t\tonComplete(data);\n\t\t\t} else if (s === 'FAILED') {\n\t\t\t\tuxState = 'failed';\n\t\t\t\tstopPolling();\n\t\t\t\tonComplete(data);\n\t\t\t} else if (s === 'CANCELLED') {\n\t\t\t\tuxState = 'cancelled';\n\t\t\t\tstopPolling();\n\t\t\t\tonComplete(data);\n\t\t\t} else {\n\t\t\t\t// Unknown status — log and keep polling\n\t\t\t\tconsole.warn('[TranslationRunProgress] Unknown status:', s);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.warn('[TranslationRunProgress] Poll error:', err);\n\t\t}\n\t}\n\n\t/** @returns {Promise} */\n\tasync function handleCancel() {\n\t\tisCancelling = true;\n\t\ttry {\n\t\t\tawait cancelRun(runId);\n\t\t\tuxState = 'cancelled';\n\t\t\tstopPolling();\n\t\t\t// Notify the parent page that the run is complete so it resets\n\t\t\t// isRunning and hides the emit-processing UI. Without this, the\n\t\t\t// page-level isRunning stays `true` and the store's independent\n\t\t\t// polling (translationRunStore) triggers a SECOND onComplete\n\t\t\t// callback, causing duplicate toasts and state thrashing.\n\t\t\tonComplete({ status: 'CANCELLED', error_message: null });\n\t\t\taddToast($t.translate?.run?.run_cancelled, 'info');\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.cancel_failed, 'error');\n\t\t} finally {\n\t\t\tisCancelling = false;\n\t\t}\n\t}\n\n\n
\n\t{#if uxState === 'idle'}\n\t\t
\n\t\t\t

{$t.translate?.run?.loading}

\n\t\t
\n\n\t{:else if isRunning}\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{uxState === 'inserting' ? $t.translate?.run?.insert_phase : $t.translate?.run?.translate_phase}\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{isCancelling ? $t.translate?.run?.cancelling : $t.translate?.run?.cancel}\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.total}\n\t\t\t\t\t

{totalRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.success}\n\t\t\t\t\t

{successfulRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.failed}\n\t\t\t\t\t

{failedRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.skipped}\n\t\t\t\t\t

{skippedRecords}

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

\n\t\t\t\t{$t.translate?.run?.batches_progress?.replace('{count}', batchCount).replace('{pct}', progressPct)}\n\t\t\t

\n\t\t
\n\n\t\n\t{:else if uxState === 'completed'}\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t{$t.translate?.run?.completed}\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.total}\n\t\t\t\t\t

{successfulRecords} / {totalRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.skipped}\n\t\t\t\t\t

{skippedRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.insert_status?.replace(':', '')}\n\t\t\t\t\t

{insertStatus || $t.translate?.run?.insert_completed}

\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'partial'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.completed_with_errors}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.retry_failed}\n\t\t\t\t\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.success}\n\t\t\t\t\t

{successfulRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.failed}\n\t\t\t\t\t

{failedRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.skipped}\n\t\t\t\t\t

{skippedRecords}

\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'failed'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.translation_failed}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.common?.retry}\n\t\t\t\t\n\t\t\t
\n\t\t\t

{status?.error_message || $t.translate?.common?.unknown}

\n\t\t
\n\n\t\n\t{:else if uxState === 'insert_failed'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.insert_failed}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.retry_insert}\n\t\t\t\t\n\t\t\t
\n\t\t\t

\n\t\t\t\t{$t.translate?.run?.completed} ({successfulRecords}) — {$t.translate?.run?.insert_failed}\n\t\t\t

\n\t\t
\n\n\t\n\t{:else if uxState === 'cancelled'}\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t{$t.translate?.run?.cancelled}\n\t\t\t
\n\t\t
\n\t{/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\t{#if uxState === 'idle'}\n\t\t
\n\t\t\t

{$t.translate?.run?.loading}

\n\t\t
\n\n\t{:else if isRunning}\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{uxState === 'inserting' ? $t.translate?.run?.insert_phase : $t.translate?.run?.translate_phase}\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{isCancelling ? $t.translate?.run?.cancelling : $t.translate?.run?.cancel}\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.total}\n\t\t\t\t\t

{totalRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.success}\n\t\t\t\t\t

{successfulRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.failed}\n\t\t\t\t\t

{failedRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.skipped}\n\t\t\t\t\t

{skippedRecords}

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

\n\t\t\t\t{$t.translate?.run?.batches_progress?.replace('{count}', batchCount).replace('{pct}', progressPct)}\n\t\t\t

\n\t\t
\n\n\t\n\t{:else if uxState === 'completed'}\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t{$t.translate?.run?.completed}\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.total}\n\t\t\t\t\t

{successfulRecords} / {totalRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.skipped}\n\t\t\t\t\t

{skippedRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.insert_status?.replace(':', '')}\n\t\t\t\t\t

{insertStatus || $t.translate?.run?.insert_completed}

\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'partial'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.completed_with_errors}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.retry_failed}\n\t\t\t\t\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.success}\n\t\t\t\t\t

{successfulRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.failed}\n\t\t\t\t\t

{failedRecords}

\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t{$t.translate?.run?.skipped}\n\t\t\t\t\t

{skippedRecords}

\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\n\t\n\t{:else if uxState === 'failed'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.translation_failed}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.common?.retry}\n\t\t\t\t\n\t\t\t
\n\t\t\t

{status?.error_message || $t.translate?.common?.unknown}

\n\t\t
\n\n\t\n\t{:else if uxState === 'insert_failed'}\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.insert_failed}\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.retry_insert}\n\t\t\t\t\n\t\t\t
\n\t\t\t

\n\t\t\t\t{$t.translate?.run?.completed} ({successfulRecords}) — {$t.translate?.run?.insert_failed}\n\t\t\t

\n\t\t
\n\n\t\n\t{:else if uxState === 'cancelled'}\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t{$t.translate?.run?.cancelled}\n\t\t\t
\n\t\t
\n\t{/if}\n
\n\n" }, { "contract_id": "TranslationRunResult", "contract_type": "Component", "file_path": "frontend/src/lib/components/translate/TranslationRunResult.svelte", "start_line": 1, - "end_line": 603, + "end_line": 465, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", - "POST": "Results displayed with actions for retry, SQL audit, and Superset reference.", - "PRE": "runId is a valid completed/failed/pending run.", - "PURPOSE": "Run result display with statistics, insert status badge, Superset query reference, SQL audit block, and retry buttons.", - "TASK": "connect_orphaned_components", + "COMPLEXITY": 3, + "PURPOSE": "Component component: lib/components/translate/TranslationRunResult.svelte", "TYPE": "{'completed'|'partial'|'failed'|'insert_failed'}", "UX_FEEDBACK": "Tabular statistics; click to copy Superset query ID; collapsible SQL block.", "UX_RECOVERY": "Retry failed batches; retry insert.", @@ -87707,20 +92995,8 @@ { "source_id": "TranslationRunResult", "relation_type": "DEPENDS_ON", - "target_id": "api_module", - "target_ref": "[api_module]" - }, - { - "source_id": "TranslationRunResult", - "relation_type": "DEPENDS_ON", - "target_id": "api_module", - "target_ref": "[api_module]" - }, - { - "source_id": "TranslationRunResult", - "relation_type": "DEPENDS_ON", - "target_id": "api_module", - "target_ref": "[api_module]" + "target_id": "TranslateApi", + "target_ref": "[TranslateApi]" }, { "source_id": "TranslationRunResult", @@ -87731,30 +93007,28 @@ ], "schema_warnings": [ { - "code": "unknown_tag", - "tag": "TASK", - "message": "@TASK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Component' at C4", + "message": "@TYPE value '{'completed'|'partial'|'failed'|'insert_failed'}' is not in allowed enum", "detail": { - "actual_complexity": 4, - "contract_type": "Component" + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{'completed'|'partial'|'failed'|'insert_failed'}" } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\t */\n\n\timport { t } from '$lib/i18n';\n\timport { addToast } from '$lib/toasts.js';\n\timport {\n\t\tfetchRunStatus,\n\t\tfetchRunRecords,\n\t\tretryFailedBatches,\n\t\tretryInsert,\n\t\tfetchRunBatches\n\t} from '$lib/api/translate.js';\n\timport CorrectionCell from './CorrectionCell.svelte';\n\timport TermCorrectionPopup from './TermCorrectionPopup.svelte';\n\timport BulkReplaceModal from './BulkReplaceModal.svelte';\n\n\t/** @type {{ runId: string, onRefresh: Function }} */\n\tlet { runId, onRefresh = () => {} } = $props();\n\n\t/**\n\t * @type {'completed'|'partial'|'failed'|'insert_failed'}\n\t */\n\tlet uxState = $state('completed');\n\tlet status = $state(null);\n\tlet records = $state([]);\n\tlet batches = $state([]);\n\tlet showRecords = $state(false);\n\tlet showSqlAudit = $state(false);\n\tlet isRetrying = $state(false);\n\tlet isRetryingInsert = $state(false);\n\tlet sqlAuditLines = $state([]);\n\tlet targetLanguages = $state([]);\n\tlet correctionPopupData = $state(null);\n\tlet showBulkReplace = $state(false);\n\n\t// Pagination & filter state\n\tlet currentPage = $state(1);\n\tlet pageSize = $state(25);\n\tlet totalRecords = $state(0);\n\tlet statusFilter = $state('');\n\n\tlet totalPages = $derived(totalRecords > 0 ? Math.ceil(totalRecords / pageSize) : 1);\n\tlet recordStart = $derived(totalRecords > 0 ? (currentPage - 1) * pageSize + 1 : 0);\n\tlet recordEnd = $derived(Math.min(currentPage * pageSize, totalRecords));\n\n\tlet visiblePages = $derived.by(() => {\n\t\tif (totalPages <= 7) {\n\t\t\treturn Array.from({ length: totalPages }, (_, i) => i + 1);\n\t\t}\n\t\tconst half = 3;\n\t\tlet start = Math.max(1, currentPage - half);\n\t\tlet end = Math.min(totalPages, currentPage + half);\n\t\tif (start === 1) end = Math.min(totalPages, start + 6);\n\t\tif (end === totalPages) start = Math.max(1, end - 6);\n\t\tconst pages = [];\n\t\tfor (let p = start; p <= end; p++) pages.push(p);\n\t\treturn pages;\n\t});\n\n\tconst STATUS_FILTERS = [\n\t\t{ value: '', labelKey: 'translate.run.filter_all', label: 'All' },\n\t\t{ value: 'FAILED', labelKey: 'translate.run.translation_failed', label: 'Failed' },\n\t\t{ value: 'SUCCESS', labelKey: 'translate.run.success', label: 'Success' },\n\t\t{ value: 'SKIPPED', labelKey: 'translate.run.skipped', label: 'Skipped' },\n\t];\n\n\tfunction getRunStatusLabel(value) {\n\t\tconst labels = {\n\t\t\tCOMPLETED: $t.translate?.run?.completed || 'Completed',\n\t\t\tFAILED: $t.translate?.run?.translation_failed || 'Failed',\n\t\t\tRUNNING: $t.translate?.history?.status_running || 'Running',\n\t\t\tPENDING: $t.translate?.history?.status_pending || 'Pending',\n\t\t\tCANCELLED: $t.translate?.run?.cancelled || 'Cancelled',\n\t\t};\n\t\treturn labels[value] || value;\n\t}\n\n\t// Derived\n\tlet insertStatusBadge = $derived.by(() => {\n\t\tconst s = status?.insert_status;\n\t\tif (!s || s === 'success') return { label: $t.translate?.run?.success, class: 'bg-green-100 text-green-700' };\n\t\tif (s === 'failed' || s === 'timeout') return { label: $t.translate?.run?.failed, class: 'bg-red-100 text-red-700' };\n\t\treturn { label: s, class: 'bg-yellow-100 text-yellow-700' };\n\t});\n\n\t$effect(() => {\n\t\tif (runId) {\n\t\t\tloadData();\n\t\t}\n\t});\n\n\t// Reload records when page, filter, or table visibility changes\n\tlet recordsLoadKey = $derived(showRecords ? `${currentPage}-${statusFilter}-${pageSize}-${runId}` : null);\n\t$effect(() => {\n\t\tif (recordsLoadKey) {\n\t\t\tloadRecords();\n\t\t}\n\t});\n\n\t/** @returns {Promise} */\n\tasync function loadData() {\n\t\ttry {\n\t\t\tconst [statusData, batchesData] = await Promise.all([\n\t\t\t\tfetchRunStatus(runId),\n\t\t\t\tfetchRunBatches(runId),\n\t\t\t]);\n\t\t\tstatus = statusData;\n\t\t\tbatches = Array.isArray(batchesData) ? batchesData : [];\n\n\t\t\t// Extract target languages from language_stats\n\t\t\tif (statusData.language_stats && statusData.language_stats.length > 0) {\n\t\t\t\ttargetLanguages = statusData.language_stats.map(l => l.language_code);\n\t\t\t}\n\n\t\t\t// Determine state\n\t\t\tconst s = statusData.status;\n\t\t\tconst insertS = statusData.insert_status;\n\t\t\tif (s === 'COMPLETED' && insertS === 'failed') {\n\t\t\t\tuxState = 'insert_failed';\n\t\t\t} else if (s === 'COMPLETED' && (statusData.failed_records || 0) > 0) {\n\t\t\t\tuxState = 'partial';\n\t\t\t} else if (s === 'FAILED') {\n\t\t\t\tuxState = 'failed';\n\t\t\t} else {\n\t\t\t\tuxState = 'completed';\n\t\t\t}\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.load_failed, 'error');\n\t\t}\n\t}\n\n\t/** @returns {Promise} */\n\tasync function loadRecords() {\n\t\ttry {\n\t\t\tconst recordsData = await fetchRunRecords(runId, {\n\t\t\t\tpage: currentPage,\n\t\t\t\tpage_size: pageSize,\n\t\t\t\tstatus: statusFilter || undefined\n\t\t\t});\n\t\t\trecords = recordsData?.items || [];\n\t\t\ttotalRecords = recordsData?.total || 0;\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.load_failed, 'error');\n\t\t}\n\t}\n\n\t/** @param {number} page */\n\tfunction goToPage(page) {\n\t\tif (page < 1 || page > totalPages) return;\n\t\tcurrentPage = page;\n\t}\n\n\t/** @param {string} filterValue */\n\tfunction setFilter(filterValue) {\n\t\tstatusFilter = filterValue;\n\t\tcurrentPage = 1;\n\t}\n\n\t/** @returns {Promise} */\n\tasync function handleRetry() {\n\t\tisRetrying = true;\n\t\ttry {\n\t\t\tawait retryFailedBatches(runId);\n\t\t\taddToast($t.translate?.run?.retry_success, 'success');\n\t\t\tawait loadData();\n\t\t\tonRefresh();\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.retry_failed_msg, 'error');\n\t\t} finally {\n\t\t\tisRetrying = false;\n\t\t}\n\t}\n\n\t/** @returns {Promise} */\n\tasync function handleRetryInsert() {\n\t\tisRetryingInsert = true;\n\t\ttry {\n\t\t\tawait retryInsert(runId);\n\t\t\taddToast($t.translate?.run?.insert_retry_success, 'success');\n\t\t\tawait loadData();\n\t\t\tonRefresh();\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.insert_retry_failed_msg, 'error');\n\t\t} finally {\n\t\t\tisRetryingInsert = false;\n\t\t}\n\t}\n\n\t/** @param {string} text */\n\tfunction copyToClipboard(text) {\n\t\tnavigator.clipboard.writeText(text).then(() => {\n\t\t\taddToast($t.translate?.run?.copy_success, 'success');\n\t\t}).catch(() => {\n\t\t\taddToast($t.translate?.run?.copy_failed, 'error');\n\t\t});\n\t}\n\n\t/** @returns {Promise} */\n\tasync function toggleSqlAudit() {\n\t\tshowSqlAudit = !showSqlAudit;\n\t\tif (showSqlAudit && sqlAuditLines.length === 0) {\n\t\t\t// Load all records with successful translation for SQL audit\n\t\t\ttry {\n\t\t\t\tconst allRecords = await fetchRunRecords(runId, { page_size: 500, status: 'SUCCESS' });\n\t\t\t\tconst items = allRecords?.items || [];\n\t\t\t\tsqlAuditLines = items.slice(0, 20).map(r => r.target_sql).filter(Boolean);\n\t\t\t} catch (err) {\n\t\t\t\tsqlAuditLines = [$t.translate?.run?.no_sql_audit];\n\t\t\t}\n\t\t}\n\t}\n\n\n
\n\t{#if !status}\n\t\t
\n\t\t\t

{$t.translate?.run?.loading_result}

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

{$t.translate?.run?.result_title}

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{#if uxState === 'failed' || uxState === 'partial'}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{isRetrying ? $t.translate?.run?.retrying : $t.translate?.run?.retry_failed}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if uxState === 'insert_failed'}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{isRetryingInsert ? $t.translate?.run?.retrying_insert : $t.translate?.run?.retry_insert}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.run?.total_records}

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

{status.total_records || 0}

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

{$t.translate?.run?.success}

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

{status.successful_records || 0}

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

{$t.translate?.run?.failed}

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

{status.failed_records || 0}

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

{$t.translate?.run?.skipped}

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

{status.skipped_records || 0}

\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if status.language_stats && status.language_stats.length > 0}\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.run?.per_language ?? 'Per-Language Statistics'}

\n\t\t\t\t\t
\n\t\t\t\t\t\t{#each status.language_stats as lang}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t

{lang.language_code}

\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{lang.translated_rows}\n\t\t\t\t\t\t\t\t\t{#if lang.failed_rows > 0}\n\t\t\t\t\t\t\t\t\t\t{lang.failed_rows}\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t{#if lang.skipped_rows > 0}\n\t\t\t\t\t\t\t\t\t\t{lang.skipped_rows}\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t{lang.total_rows}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.run_id}\n\t\t\t\t\t\t{status.id}\n\t\t\t\t\t\t copyToClipboard(status.id)}\n\t\t\t\t\t\t\tclass=\"ml-1 text-blue-500 hover:text-blue-700 text-xs\"\n\t\t\t\t\t\t\ttitle={$t.translate?.run?.copy_id}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{$t.translate?.run?.copy_id}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.status_label || 'Статус'}\n\t\t\t\t\t\t{getRunStatusLabel(status.status)}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.insert_status}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{insertStatusBadge.label}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t{#if status.superset_execution_id}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.run?.superset_query}\n\t\t\t\t\t\t\t{status.superset_execution_id}\n\t\t\t\t\t\t\t copyToClipboard(status.superset_execution_id)}\n\t\t\t\t\t\t\t\tclass=\"ml-1 text-blue-500 hover:text-blue-700 text-xs\"\n\t\t\t\t\t\t\t\ttitle={$t.translate?.run?.copy_id}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{$t.translate?.run?.copy_id}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if status.error_message}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.run?.error_label?.replace('{error}', status.error_message)}\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.sql_audit?.replace('{count}', sqlAuditLines.length)}\n\t\t\t\t\n\n\t\t\t\t{#if showSqlAudit}\n\t\t\t\t\t
\n\t\t\t\t\t\t{#if sqlAuditLines.length === 0}\n\t\t\t\t\t\t\t

{$t.translate?.run?.no_sql_audit}

\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t{#each sqlAuditLines as line, idx}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{idx + 1}. {line}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if status.event_invariants}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.event_invariants}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{status.event_invariants.invariant_valid ? $t.translate?.run?.valid : $t.translate?.run?.violated}\n\t\t\t\t\t\t\n\t\t\t\t\t\t|\n\t\t\t\t\t\t{$t.translate?.run?.superset_query_id_label} {status.event_invariants.has_run_started ? $t.translate?.run?.started_yes : $t.translate?.run?.started_no}\n\t\t\t\t\t\t|\n\t\t\t\t\t\t{$t.translate?.run?.terminal_events?.replace('{count}', status.event_invariants.terminal_event_count)}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t{#if status && status.total_records > 0}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\tshowRecords = !showRecords;\n\t\t\t\t\t\t\t\tif (showRecords) { currentPage = 1; statusFilter = ''; }\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclass=\"flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{$t.translate?.run?.records_title || 'Translation Records'}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t({totalRecords > 0 ? `${recordStart}–${recordEnd} / ${totalRecords}` : status.total_records})\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t{#if showRecords}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{#each STATUS_FILTERS as f}\n\t\t\t\t\t\t\t\t\t setFilter(f.value)}\n\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-[11px] rounded-full border transition-colors\n\t\t\t\t\t\t\t\t\t\t\t{statusFilter === f.value\n\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-blue-600 text-white border-blue-600'\n\t\t\t\t\t\t\t\t\t\t\t\t: 'text-gray-500 border-gray-300 hover:bg-gray-100'}\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{$t.translate?.run?.[f.labelKey] || f.label}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\n\t\t\t\t\t{#if showRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\t{#if targetLanguages.length === 0}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#each records as row, idx}\n\t\t\t\t\t\t\t\t\t\t{@const globalIdx = (currentPage - 1) * pageSize + idx + 1}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#if targetLanguages.length > 0}\n\t\t\t\t\t\t\t\t\t\t\t\t{#if row.languages && row.languages.length > 0}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{@const langData = (row.languages || []).find(l => l.language_code === lang)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{@const cellValue = langData?.final_value || langData?.translated_value || ''}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{@const detectedSource = langData?.source_language_detected || row.source_language_detected || ''}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
#{$t.translate?.preview?.table_source || 'Source'}\n\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t
{$t.translate?.preview?.translation || 'Translation'}{$t.translate?.preview?.table_status || 'Status'}{$t.translate?.preview?.table_actions || 'Actions'}
{globalIdx}\n\t\t\t\t\t\t\t\t\t\t\t\t{row.source_sql || row.source_object_name || '—'}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.run?.translation_failed || 'Translation failed'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.status}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{#if row.languages && row.languages.length > 0}\n\t\t\t\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst firstLang = row.languages?.[0] || {};\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcorrectionPopupData = {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsourceTerm: row.source_sql || row.source_object_name || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tincorrectTarget: firstLang?.final_value || firstLang?.translated_value || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsourceLanguage: firstLang?.source_language_detected || row.source_language_detected || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttargetLanguage: targetLanguages[0] || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trunId,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trowKey: row.id\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-[10px] bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttitle=\"Submit Correction\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.run?.correct || 'Correct'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\n\t\t\t\t\t\t{#if totalPages > 1}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{$t.translate?.run?.records_title || 'Records'}: {recordStart}–{recordEnd} / {totalRecords}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t goToPage(currentPage - 1)}\n\t\t\t\t\t\t\t\t\t\tdisabled={currentPage <= 1}\n\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-30 disabled:cursor-default transition-colors\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t‹\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#each visiblePages as page}\n\t\t\t\t\t\t\t\t\t\t goToPage(page)}\n\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs border rounded transition-colors\n\t\t\t\t\t\t\t\t\t\t\t\t{page === currentPage\n\t\t\t\t\t\t\t\t\t\t\t\t\t? 'bg-blue-600 text-white border-blue-600'\n\t\t\t\t\t\t\t\t\t\t\t\t\t: 'border-gray-300 text-gray-600 hover:bg-gray-100'}\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{page}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t goToPage(currentPage + 1)}\n\t\t\t\t\t\t\t\t\t\tdisabled={currentPage >= totalPages}\n\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-30 disabled:cursor-default transition-colors\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t›\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t{/if}\n\t\t
\n\t{/if}\n
\n\n\n correctionPopupData = null}\n\tonSubmitted={() => { correctionPopupData = null; loadData(); onRefresh(); }}\n/>\n\n showBulkReplace = false}\n\tonApplied={(count) => { showBulkReplace = false; loadData(); onRefresh(); }}\n/>\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n
\n\t{#if !status}\n\t\t
\n\t\t\t

{$t.translate?.run?.loading_result}

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

{$t.translate?.run?.result_title}

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t{#if uxState === 'failed' || uxState === 'partial'}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{isRetrying ? $t.translate?.run?.retrying : $t.translate?.run?.retry_failed}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if uxState === 'insert_failed'}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{isRetryingInsert ? $t.translate?.run?.retrying_insert : $t.translate?.run?.retry_insert}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.run?.total_records}

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

{status.total_records || 0}

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

{$t.translate?.run?.success}

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

{status.successful_records || 0}

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

{$t.translate?.run?.failed}

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

{status.failed_records || 0}

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

{$t.translate?.run?.skipped}

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

{status.skipped_records || 0}

\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if status.language_stats && status.language_stats.length > 0}\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.run?.per_language ?? 'Per-Language Statistics'}

\n\t\t\t\t\t
\n\t\t\t\t\t\t{#each status.language_stats as lang}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t

{lang.language_code}

\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{lang.translated_rows}\n\t\t\t\t\t\t\t\t\t{#if lang.failed_rows > 0}\n\t\t\t\t\t\t\t\t\t\t{lang.failed_rows}\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t{#if lang.skipped_rows > 0}\n\t\t\t\t\t\t\t\t\t\t{lang.skipped_rows}\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t{lang.total_rows}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.run_id}\n\t\t\t\t\t\t{status.id}\n\t\t\t\t\t\t copyToClipboard(status.id)}\n\t\t\t\t\t\t\tclass=\"ml-1 text-blue-500 hover:text-blue-700 text-xs\"\n\t\t\t\t\t\t\ttitle={$t.translate?.run?.copy_id}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{$t.translate?.run?.copy_id}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.status_label || 'Статус'}\n\t\t\t\t\t\t{getRunStatusLabel(status.status)}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.insert_status}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{insertStatusBadge.label}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t{#if status.superset_execution_id}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.run?.superset_query}\n\t\t\t\t\t\t\t{status.superset_execution_id}\n\t\t\t\t\t\t\t copyToClipboard(status.superset_execution_id)}\n\t\t\t\t\t\t\t\tclass=\"ml-1 text-blue-500 hover:text-blue-700 text-xs\"\n\t\t\t\t\t\t\t\ttitle={$t.translate?.run?.copy_id}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{$t.translate?.run?.copy_id}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if status.error_message}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.run?.error_label?.replace('{error}', status.error_message)}\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t{$t.translate?.run?.sql_audit?.replace('{count}', sqlAuditLines.length)}\n\t\t\t\t\n\n\t\t\t\t{#if showSqlAudit}\n\t\t\t\t\t
\n\t\t\t\t\t\t{#if sqlAuditLines.length === 0}\n\t\t\t\t\t\t\t

{$t.translate?.run?.no_sql_audit}

\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t{#each sqlAuditLines as line, idx}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{idx + 1}. {line}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t
\n\n\t\t\t\n\t\t\t{#if status.event_invariants}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.run?.event_invariants}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{status.event_invariants.invariant_valid ? $t.translate?.run?.valid : $t.translate?.run?.violated}\n\t\t\t\t\t\t\n\t\t\t\t\t\t|\n\t\t\t\t\t\t{$t.translate?.run?.superset_query_id_label} {status.event_invariants.has_run_started ? $t.translate?.run?.started_yes : $t.translate?.run?.started_no}\n\t\t\t\t\t\t|\n\t\t\t\t\t\t{$t.translate?.run?.terminal_events?.replace('{count}', status.event_invariants.terminal_event_count)}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t{/if}\n\n\t\t\t\n\t\t\t{#if records.length > 0}\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t showRecords = !showRecords}\n\t\t\t\t\t\t\tclass=\"flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{$t.translate?.run?.records_title || 'Translation Records'} ({records.length})\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t{#if showRecords}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\t{#if targetLanguages.length === 0}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#each records as row, idx}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{#if targetLanguages.length > 0}\n\t\t\t\t\t\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{@const langData = (row.languages || []).find(l => l.language_code === lang)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{@const cellValue = langData?.final_value || langData?.translated_value || ''}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{@const detectedSource = langData?.source_language_detected || row.source_language_detected || ''}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
#{$t.translate?.preview?.table_source || 'Source'}\n\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t\t
{$t.translate?.preview?.translation || 'Translation'}{$t.translate?.preview?.table_status || 'Status'}{$t.translate?.preview?.table_actions || 'Actions'}
{idx + 1}\n\t\t\t\t\t\t\t\t\t\t\t\t{row.source_sql || row.source_object_name || '—'}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.status}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst firstLang = row.languages?.[0] || {};\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcorrectionPopupData = {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsourceTerm: row.source_sql || row.source_object_name || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tincorrectTarget: firstLang?.final_value || firstLang?.translated_value || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsourceLanguage: firstLang?.source_language_detected || row.source_language_detected || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttargetLanguage: targetLanguages[0] || '',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trunId,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\trowKey: row.id\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"px-2 py-0.5 text-[10px] bg-primary text-white rounded hover:bg-primary-hover transition-colors\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle=\"Submit Correction\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{$t.translate?.run?.correct || 'Correct'}\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t{/if}\n\t\t
\n\t{/if}\n
\n\n\n correctionPopupData = null}\n\tonSubmitted={() => { correctionPopupData = null; loadData(); onRefresh(); }}\n/>\n\n showBulkReplace = false}\n\tonApplied={(count) => { showBulkReplace = false; loadData(); onRefresh(); }}\n/>\n\n" }, { "contract_id": "BulkReplaceModalTests", @@ -91423,7 +96697,7 @@ "contract_type": "Store", "file_path": "frontend/src/lib/stores/health.js", "start_line": 1, - "end_line": 90, + "end_line": 99, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -91525,14 +96799,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region HealthStore [C:3] [TYPE Store] [SEMANTICS health, store, dashboard, summary, badge]\n// @BRIEF Manage dashboard health summary state and failing counts for UI badges.\n// @RELATION DEPENDS_ON -> [ApiModule]\n// @UX_STATE Loading -> Loading spinner, badges hidden\n// @UX_STATE Ready -> Health summary displayed with pass/warn/fail counts\n// @UX_STATE Error -> Error state with retry option\n\n// #region health_store:Store [TYPE Function]\n// @UX_STATE: Loading -> Default\n// @PURPOSE: Manage dashboard health summary state and failing counts for UI badges.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { writable, derived } from 'svelte/store';\nimport { api } from '../api.js';\n\n/**\n * @typedef {Object} HealthState\n * @property {Array} items - List of dashboard health items\n * @property {number} pass_count - Number of passing dashboards\n * @property {number} warn_count - Number of warning dashboards\n * @property {number} fail_count - Number of failing dashboards\n * @property {number} unknown_count - Number of unknown status dashboards\n * @property {boolean} loading - Loading state\n * @property {Date|null} lastUpdated - Last successful fetch timestamp\n */\n\nfunction createHealthStore() {\n const { subscribe, set, update } = writable({\n items: [],\n pass_count: 0,\n warn_count: 0,\n fail_count: 0,\n unknown_count: 0,\n loading: false,\n lastUpdated: null\n });\n let inflightRefresh = null;\n let inflightEnvironmentId = null;\n\n /**\n * Refresh health summary from API\n * @param {string|null} environmentId - Optional environment filter\n */\n async function refresh(environmentId = null) {\n const normalizedEnvironmentId = environmentId || null;\n if (inflightRefresh && inflightEnvironmentId === normalizedEnvironmentId) {\n return inflightRefresh;\n }\n\n update(s => ({ ...s, loading: true }));\n inflightEnvironmentId = normalizedEnvironmentId;\n inflightRefresh = (async () => {\n try {\n console.log(`[HealthStore][Action] Refreshing health summary context={{'environmentId': '${environmentId}'}}`);\n const summary = await api.getHealthSummary(environmentId);\n set({\n ...summary,\n loading: false,\n lastUpdated: new Date()\n });\n return summary;\n } catch (error) {\n console.error('[HealthStore][Coherence:Failed] Failed to fetch health summary:', error);\n update(s => ({ ...s, loading: false }));\n throw error;\n } finally {\n inflightRefresh = null;\n inflightEnvironmentId = null;\n }\n })();\n return inflightRefresh;\n }\n\n return {\n subscribe,\n refresh\n };\n}\n\nexport const healthStore = createHealthStore();\n\n/**\n * Derived store for the total count of failing dashboards.\n * Used for sidebar badges and global notifications.\n */\nexport const failingCount = derived(healthStore, $health => $health.fail_count);\n\n// #endregion health_store:Store\n// #endregion HealthStore\n" + "body": "// #region HealthStore [C:3] [TYPE Store] [SEMANTICS health, store, dashboard, summary, badge]\n// @BRIEF Manage dashboard health summary state and failing counts for UI badges.\n// @RELATION DEPENDS_ON -> [ApiModule]\n// @UX_STATE Loading -> Loading spinner, badges hidden\n// @UX_STATE Ready -> Health summary displayed with pass/warn/fail counts\n// @UX_STATE Error -> Error state with retry option\n\n// #region health_store:Store [TYPE Function]\n// @UX_STATE: Loading -> Default\n// @PURPOSE: Manage dashboard health summary state and failing counts for UI badges.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { writable, derived } from 'svelte/store';\nimport { api } from '../api.js';\n\n/**\n * @typedef {Object} HealthState\n * @property {Array} items - List of dashboard health items\n * @property {number} pass_count - Number of passing dashboards\n * @property {number} warn_count - Number of warning dashboards\n * @property {number} fail_count - Number of failing dashboards\n * @property {number} unknown_count - Number of unknown status dashboards\n * @property {boolean} loading - Loading state\n * @property {Date|null} lastUpdated - Last successful fetch timestamp\n */\n\nfunction createHealthStore() {\n const { subscribe, set, update } = writable({\n items: [],\n pass_count: 0,\n warn_count: 0,\n fail_count: 0,\n unknown_count: 0,\n loading: false,\n lastUpdated: null\n });\n let inflightRefresh = null;\n let inflightEnvironmentId = null;\n /** @type {boolean} - Set to true when the health monitor feature is disabled on the backend. Prevents further polling. */\n let _isDisabled = false;\n\n /**\n * Refresh health summary from API\n * @param {string|null} environmentId - Optional environment filter\n */\n async function refresh(environmentId = null) {\n if (_isDisabled) return null;\n\n const normalizedEnvironmentId = environmentId || null;\n if (inflightRefresh && inflightEnvironmentId === normalizedEnvironmentId) {\n return inflightRefresh;\n }\n\n update(s => ({ ...s, loading: true }));\n inflightEnvironmentId = normalizedEnvironmentId;\n inflightRefresh = (async () => {\n try {\n console.log(`[HealthStore][Action] Refreshing health summary context={{'environmentId': '${environmentId}'}}`);\n const summary = await api.getHealthSummary(environmentId);\n set({\n ...summary,\n loading: false,\n lastUpdated: new Date()\n });\n return summary;\n } catch (error) {\n const msg = error?.message || String(error);\n if (msg.includes('Health monitor feature is disabled') || msg.includes('404')) {\n console.warn('[HealthStore][Coherence:Disabled] Health monitor feature is disabled — stopping polling');\n _isDisabled = true;\n }\n console.error('[HealthStore][Coherence:Failed] Failed to fetch health summary:', error);\n update(s => ({ ...s, loading: false }));\n return null;\n } finally {\n inflightRefresh = null;\n inflightEnvironmentId = null;\n }\n })();\n return inflightRefresh;\n }\n\n return {\n subscribe,\n refresh\n };\n}\n\nexport const healthStore = createHealthStore();\n\n/**\n * Derived store for the total count of failing dashboards.\n * Used for sidebar badges and global notifications.\n */\nexport const failingCount = derived(healthStore, $health => $health.fail_count);\n\n// #endregion health_store:Store\n// #endregion HealthStore\n" }, { "contract_id": "health_store:Store", "contract_type": "Function", "file_path": "frontend/src/lib/stores/health.js", "start_line": 8, - "end_line": 89, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -91617,7 +96891,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region health_store:Store [TYPE Function]\n// @UX_STATE: Loading -> Default\n// @PURPOSE: Manage dashboard health summary state and failing counts for UI badges.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { writable, derived } from 'svelte/store';\nimport { api } from '../api.js';\n\n/**\n * @typedef {Object} HealthState\n * @property {Array} items - List of dashboard health items\n * @property {number} pass_count - Number of passing dashboards\n * @property {number} warn_count - Number of warning dashboards\n * @property {number} fail_count - Number of failing dashboards\n * @property {number} unknown_count - Number of unknown status dashboards\n * @property {boolean} loading - Loading state\n * @property {Date|null} lastUpdated - Last successful fetch timestamp\n */\n\nfunction createHealthStore() {\n const { subscribe, set, update } = writable({\n items: [],\n pass_count: 0,\n warn_count: 0,\n fail_count: 0,\n unknown_count: 0,\n loading: false,\n lastUpdated: null\n });\n let inflightRefresh = null;\n let inflightEnvironmentId = null;\n\n /**\n * Refresh health summary from API\n * @param {string|null} environmentId - Optional environment filter\n */\n async function refresh(environmentId = null) {\n const normalizedEnvironmentId = environmentId || null;\n if (inflightRefresh && inflightEnvironmentId === normalizedEnvironmentId) {\n return inflightRefresh;\n }\n\n update(s => ({ ...s, loading: true }));\n inflightEnvironmentId = normalizedEnvironmentId;\n inflightRefresh = (async () => {\n try {\n console.log(`[HealthStore][Action] Refreshing health summary context={{'environmentId': '${environmentId}'}}`);\n const summary = await api.getHealthSummary(environmentId);\n set({\n ...summary,\n loading: false,\n lastUpdated: new Date()\n });\n return summary;\n } catch (error) {\n console.error('[HealthStore][Coherence:Failed] Failed to fetch health summary:', error);\n update(s => ({ ...s, loading: false }));\n throw error;\n } finally {\n inflightRefresh = null;\n inflightEnvironmentId = null;\n }\n })();\n return inflightRefresh;\n }\n\n return {\n subscribe,\n refresh\n };\n}\n\nexport const healthStore = createHealthStore();\n\n/**\n * Derived store for the total count of failing dashboards.\n * Used for sidebar badges and global notifications.\n */\nexport const failingCount = derived(healthStore, $health => $health.fail_count);\n\n// #endregion health_store:Store\n" + "body": "// #region health_store:Store [TYPE Function]\n// @UX_STATE: Loading -> Default\n// @PURPOSE: Manage dashboard health summary state and failing counts for UI badges.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { writable, derived } from 'svelte/store';\nimport { api } from '../api.js';\n\n/**\n * @typedef {Object} HealthState\n * @property {Array} items - List of dashboard health items\n * @property {number} pass_count - Number of passing dashboards\n * @property {number} warn_count - Number of warning dashboards\n * @property {number} fail_count - Number of failing dashboards\n * @property {number} unknown_count - Number of unknown status dashboards\n * @property {boolean} loading - Loading state\n * @property {Date|null} lastUpdated - Last successful fetch timestamp\n */\n\nfunction createHealthStore() {\n const { subscribe, set, update } = writable({\n items: [],\n pass_count: 0,\n warn_count: 0,\n fail_count: 0,\n unknown_count: 0,\n loading: false,\n lastUpdated: null\n });\n let inflightRefresh = null;\n let inflightEnvironmentId = null;\n /** @type {boolean} - Set to true when the health monitor feature is disabled on the backend. Prevents further polling. */\n let _isDisabled = false;\n\n /**\n * Refresh health summary from API\n * @param {string|null} environmentId - Optional environment filter\n */\n async function refresh(environmentId = null) {\n if (_isDisabled) return null;\n\n const normalizedEnvironmentId = environmentId || null;\n if (inflightRefresh && inflightEnvironmentId === normalizedEnvironmentId) {\n return inflightRefresh;\n }\n\n update(s => ({ ...s, loading: true }));\n inflightEnvironmentId = normalizedEnvironmentId;\n inflightRefresh = (async () => {\n try {\n console.log(`[HealthStore][Action] Refreshing health summary context={{'environmentId': '${environmentId}'}}`);\n const summary = await api.getHealthSummary(environmentId);\n set({\n ...summary,\n loading: false,\n lastUpdated: new Date()\n });\n return summary;\n } catch (error) {\n const msg = error?.message || String(error);\n if (msg.includes('Health monitor feature is disabled') || msg.includes('404')) {\n console.warn('[HealthStore][Coherence:Disabled] Health monitor feature is disabled — stopping polling');\n _isDisabled = true;\n }\n console.error('[HealthStore][Coherence:Failed] Failed to fetch health summary:', error);\n update(s => ({ ...s, loading: false }));\n return null;\n } finally {\n inflightRefresh = null;\n inflightEnvironmentId = null;\n }\n })();\n return inflightRefresh;\n }\n\n return {\n subscribe,\n refresh\n };\n}\n\nexport const healthStore = createHealthStore();\n\n/**\n * Derived store for the total count of failing dashboards.\n * Used for sidebar badges and global notifications.\n */\nexport const failingCount = derived(healthStore, $health => $health.fail_count);\n\n// #endregion health_store:Store\n" }, { "contract_id": "frontend/src/lib/stores/health.js::createHealthStore", @@ -91638,8 +96912,8 @@ "contract_id": "frontend/src/lib/stores/health.js::refresh", "contract_type": "Function", "file_path": "frontend/src/lib/stores/health.js", - "start_line": 41, - "end_line": 45, + "start_line": 43, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -92225,7 +97499,7 @@ "contract_type": "Store", "file_path": "frontend/src/lib/stores/translationRun.js", "start_line": 1, - "end_line": 195, + "end_line": 198, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -92304,14 +97578,14 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region TranslationRunStore [C:3] [TYPE Store] [SEMANTICS translate, run, progress, polling, store]\n// @BRIEF Global store for active translation run progress — survives page navigation.\n// @TYPEDEF {'idle'|'running'|'inserting'|'completed'|'partial'|'failed'|'cancelled'} UxState\n\n */\n\n/**\n * @typedef {Object} TranslationRunState\n * @property {string|null} runId - Active translation run ID\n * @property {UxState} uxState - Current UX state\n * @property {Object|null} status - Raw status data from API\n * @property {number} totalRecords\n * @property {number} successfulRecords\n * @property {number} failedRecords\n * @property {number} skippedRecords\n * @property {number} progressPct - 0-100\n * @property {string|null} insertStatus\n * @property {number} batchCount\n * @property {string|null} jobId - The job this run belongs to (for navigation)\n * @property {boolean} isFullRun\n * @property {boolean} isActive - Derived: true when polling is active\n */\n\nconst initialState = {\n\trunId: null,\n\tuxState: 'idle',\n\tstatus: null,\n\ttotalRecords: 0,\n\tsuccessfulRecords: 0,\n\tfailedRecords: 0,\n\tskippedRecords: 0,\n\tprogressPct: 0,\n\tinsertStatus: null,\n\tbatchCount: 0,\n\tjobId: null,\n\tisFullRun: false,\n};\n\n/** @type {import('svelte/store').Writable} */\nexport const translationRunStore = writable(initialState);\n\n/** Derived: true when a run is actively being polled */\nexport const isTranslationActive = derived(\n\ttranslationRunStore,\n\t($store) => $store.uxState === 'running' || $store.uxState === 'inserting'\n);\n\n/** Derived: true when run has finished */\nexport const isTranslationFinished = derived(\n\ttranslationRunStore,\n\t($store) =>\n\t\t$store.uxState === 'completed' ||\n\t\t$store.uxState === 'partial' ||\n\t\t$store.uxState === 'failed' ||\n\t\t$store.uxState === 'cancelled'\n);\n\n// Internal polling state (not reactive)\nlet _pollingInterval = null;\nlet _pollCount = 0;\nconst MAX_POLLS = 300;\nlet _onCompleteCallback = null;\n\n/**\n * Clear the onComplete callback without stopping polling.\n * Used by page components when they unmount — the store keeps polling\n * so the global indicator still works, but the page callback is detached.\n */\nexport function clearOnCompleteCallback() {\n\t_onCompleteCallback = null;\n}\n\n/**\n * Start polling for a translation run.\n * @param {string} runId\n * @param {Object} [options]\n * @param {string} [options.jobId] - Job ID for navigation\n * @param {boolean} [options.isFullRun]\n * @param {Function} [options.onComplete] - Called when run reaches terminal state\n */\nexport function startTranslationRun(runId, options = {}) {\n\tif (!runId) return;\n\n\tstopTranslationRun();\n\n\ttranslationRunStore.set({\n\t\t...initialState,\n\t\trunId,\n\t\tuxState: 'running',\n\t\tjobId: options.jobId || null,\n\t\tisFullRun: options.isFullRun || false,\n\t});\n\n\t_onCompleteCallback = options.onComplete || null;\n\t_pollCount = 0;\n\t_pollingInterval = setInterval(pollStatus, 2000);\n\tpollStatus();\n}\n\n/**\n * Stop polling without clearing state (run may persist).\n */\nexport function stopTranslationRun() {\n\tif (_pollingInterval) {\n\t\tclearInterval(_pollingInterval);\n\t\t_pollingInterval = null;\n\t}\n}\n\n/**\n * Reset store to idle.\n */\nexport function resetTranslationRun() {\n\tstopTranslationRun();\n\ttranslationRunStore.set(initialState);\n\t_onCompleteCallback = null;\n}\n\n/**\n * Manually set the run state (used by page component for lifecycle hooks).\n * @param {Partial} state\n */\nexport function updateTranslationRunState(state) {\n\ttranslationRunStore.update(prev => ({ ...prev, ...state }));\n}\n\n/** @returns {Promise} */\nasync function pollStatus() {\n\t_pollCount++;\n\tif (_pollCount > MAX_POLLS) {\n\t\tconsole.warn('[translationRunStore] Max polls reached, stopping');\n\t\tstopTranslationRun();\n\t\ttranslationRunStore.update(s => ({ ...s, uxState: 'failed' }));\n\t\tif (_onCompleteCallback) _onCompleteCallback({ status: 'TIMEOUT', error_message: 'Translation run timed out' });\n\t\treturn;\n\t}\n\n\tlet currentState;\n\ttranslationRunStore.subscribe(s => { currentState = s; })();\n\tif (!currentState || !currentState.runId) {\n\t\tstopTranslationRun();\n\t\treturn;\n\t}\n\n\ttry {\n\t\tconst data = await fetchRunStatus(currentState.runId);\n\t\tif (!data) return;\n\n\t\tconst s = data?.status;\n\t\tconst insertS = data?.insert_status;\n\n\t\tlet newUxState = currentState.uxState;\n\n\t\tif (s === 'PENDING' || s === 'RUNNING') {\n\t\t\tnewUxState = (insertS === 'started' || insertS === 'pending' || insertS === 'running')\n\t\t\t\t? 'inserting' : 'running';\n\t\t} else if (s === 'COMPLETED') {\n\t\t\tnewUxState = (insertS === 'failed' || insertS === 'timeout') ? 'insert_failed'\n\t\t\t\t: (data.failed_records > 0) ? 'partial' : 'completed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'FAILED') {\n\t\t\tnewUxState = 'failed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'CANCELLED') {\n\t\t\tnewUxState = 'cancelled';\n\t\t\tstopTranslationRun();\n\t\t}\n\n\t\tconst total = data?.total_records || 0;\n\t\tconst pct = total > 0\n\t\t\t? Math.round(((data.successful_records + data.failed_records + data.skipped_records) / total) * 100)\n\t\t\t: 0;\n\n\t\ttranslationRunStore.update(prev => ({\n\t\t\t...prev,\n\t\t\tuxState: newUxState,\n\t\t\tstatus: data,\n\t\t\ttotalRecords: total,\n\t\t\tsuccessfulRecords: data?.successful_records || 0,\n\t\t\tfailedRecords: data?.failed_records || 0,\n\t\t\tskippedRecords: data?.skipped_records || 0,\n\t\t\tprogressPct: pct,\n\t\t\tinsertStatus: data?.insert_status || null,\n\t\t\tbatchCount: data?.batch_count || 0,\n\t\t}));\n\n\t\t// Fire completion callback if terminal\n\t\tif (newUxState !== 'running' && newUxState !== 'inserting') {\n\t\t\tif (_onCompleteCallback) _onCompleteCallback(data);\n\t\t}\n\t} catch (err) {\n\t\tconsole.warn('[translationRunStore] Poll error:', err);\n\t}\n}\n// #endregion TranslationRunStore\n" + "body": "// #region TranslationRunStore [C:3] [TYPE Store] [SEMANTICS translate, run, progress, polling, store]\n// @BRIEF Global store for active translation run progress — survives page navigation.\n// @TYPEDEF {'idle'|'running'|'inserting'|'completed'|'partial'|'failed'|'insert_failed'|'cancelled'} UxState\n\n\n/**\n * @typedef {Object} TranslationRunState\n * @property {string|null} runId - Active translation run ID\n * @property {UxState} uxState - Current UX state\n * @property {Object|null} status - Raw status data from API\n * @property {number} totalRecords\n * @property {number} successfulRecords\n * @property {number} failedRecords\n * @property {number} skippedRecords\n * @property {number} progressPct - 0-100\n * @property {string|null} insertStatus\n * @property {number} batchCount\n * @property {string|null} jobId - The job this run belongs to (for navigation)\n * @property {boolean} isFullRun\n * @property {boolean} isActive - Derived: true when polling is active\n */\n\nimport { writable, derived } from 'svelte/store';\nimport { fetchRunStatus } from '$lib/api/translate.js';\n\nconst initialState = {\n\trunId: null,\n\tuxState: 'idle',\n\tstatus: null,\n\ttotalRecords: 0,\n\tsuccessfulRecords: 0,\n\tfailedRecords: 0,\n\tskippedRecords: 0,\n\tprogressPct: 0,\n\tinsertStatus: null,\n\tbatchCount: 0,\n\tjobId: null,\n\tisFullRun: false,\n};\n\n/** @type {import('svelte/store').Writable} */\nexport const translationRunStore = writable(initialState);\n\n/** Derived: true when a run is actively being polled */\nexport const isTranslationActive = derived(\n\ttranslationRunStore,\n\t($store) => $store.uxState === 'running' || $store.uxState === 'inserting'\n);\n\n/** Derived: true when run has finished */\nexport const isTranslationFinished = derived(\n\ttranslationRunStore,\n\t($store) =>\n\t\t$store.uxState === 'completed' ||\n\t\t$store.uxState === 'partial' ||\n\t\t$store.uxState === 'failed' ||\n\t\t$store.uxState === 'insert_failed' ||\n\t\t$store.uxState === 'cancelled'\n);\n\n// Internal polling state (not reactive)\nlet _pollingInterval = null;\nlet _pollCount = 0;\nconst MAX_POLLS = 300;\nlet _onCompleteCallback = null;\n\n/**\n * Clear the onComplete callback without stopping polling.\n * Used by page components when they unmount — the store keeps polling\n * so the global indicator still works, but the page callback is detached.\n */\nexport function clearOnCompleteCallback() {\n\t_onCompleteCallback = null;\n}\n\n/**\n * Start polling for a translation run.\n * @param {string} runId\n * @param {Object} [options]\n * @param {string} [options.jobId] - Job ID for navigation\n * @param {boolean} [options.isFullRun]\n * @param {Function} [options.onComplete] - Called when run reaches terminal state\n */\nexport function startTranslationRun(runId, options = {}) {\n\tif (!runId) return;\n\n\tstopTranslationRun();\n\n\ttranslationRunStore.set({\n\t\t...initialState,\n\t\trunId,\n\t\tuxState: 'running',\n\t\tjobId: options.jobId || null,\n\t\tisFullRun: options.isFullRun || false,\n\t});\n\n\t_onCompleteCallback = options.onComplete || null;\n\t_pollCount = 0;\n\t_pollingInterval = setInterval(pollStatus, 2000);\n\tpollStatus();\n}\n\n/**\n * Stop polling without clearing state (run may persist).\n */\nexport function stopTranslationRun() {\n\tif (_pollingInterval) {\n\t\tclearInterval(_pollingInterval);\n\t\t_pollingInterval = null;\n\t}\n}\n\n/**\n * Reset store to idle.\n */\nexport function resetTranslationRun() {\n\tstopTranslationRun();\n\ttranslationRunStore.set(initialState);\n\t_onCompleteCallback = null;\n}\n\n/**\n * Manually set the run state (used by page component for lifecycle hooks).\n * @param {Partial} state\n */\nexport function updateTranslationRunState(state) {\n\ttranslationRunStore.update(prev => ({ ...prev, ...state }));\n}\n\n/** @returns {Promise} */\nasync function pollStatus() {\n\t_pollCount++;\n\tif (_pollCount > MAX_POLLS) {\n\t\tconsole.warn('[translationRunStore] Max polls reached, stopping');\n\t\tstopTranslationRun();\n\t\ttranslationRunStore.update(s => ({ ...s, uxState: 'failed' }));\n\t\tif (_onCompleteCallback) _onCompleteCallback({ status: 'TIMEOUT', error_message: 'Translation run timed out' });\n\t\treturn;\n\t}\n\n\tlet currentState;\n\ttranslationRunStore.subscribe(s => { currentState = s; })();\n\tif (!currentState || !currentState.runId) {\n\t\tstopTranslationRun();\n\t\treturn;\n\t}\n\n\ttry {\n\t\tconst data = await fetchRunStatus(currentState.runId);\n\t\tif (!data) return;\n\n\t\tconst s = data?.status;\n\t\tconst insertS = data?.insert_status;\n\n\t\tlet newUxState = currentState.uxState;\n\n\t\tif (s === 'PENDING' || s === 'RUNNING') {\n\t\t\tnewUxState = (insertS === 'started' || insertS === 'pending' || insertS === 'running')\n\t\t\t\t? 'inserting' : 'running';\n\t\t} else if (s === 'COMPLETED') {\n\t\t\tnewUxState = (insertS === 'failed' || insertS === 'timeout') ? 'insert_failed'\n\t\t\t\t: (data.failed_records > 0) ? 'partial' : 'completed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'FAILED') {\n\t\t\tnewUxState = 'failed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'CANCELLED') {\n\t\t\tnewUxState = 'cancelled';\n\t\t\tstopTranslationRun();\n\t\t}\n\n\t\tconst total = data?.total_records || 0;\n\t\tconst pct = total > 0\n\t\t\t? Math.round(((data.successful_records + data.failed_records + data.skipped_records) / total) * 100)\n\t\t\t: 0;\n\n\t\ttranslationRunStore.update(prev => ({\n\t\t\t...prev,\n\t\t\tuxState: newUxState,\n\t\t\tstatus: data,\n\t\t\ttotalRecords: total,\n\t\t\tsuccessfulRecords: data?.successful_records || 0,\n\t\t\tfailedRecords: data?.failed_records || 0,\n\t\t\tskippedRecords: data?.skipped_records || 0,\n\t\t\tprogressPct: pct,\n\t\t\tinsertStatus: data?.insert_status || null,\n\t\t\tbatchCount: data?.batch_count || 0,\n\t\t}));\n\n\t\t// Fire completion callback if terminal\n\t\tif (newUxState !== 'running' && newUxState !== 'inserting') {\n\t\t\tif (_onCompleteCallback) _onCompleteCallback(data);\n\t\t}\n\t} catch (err) {\n\t\tconsole.warn('[translationRunStore] Poll error:', err);\n\t}\n}\n// #endregion TranslationRunStore\n" }, { "contract_id": "frontend/src/lib/stores/translationRun.js::clearOnCompleteCallback", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 64, - "end_line": 69, + "start_line": 67, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -92325,8 +97599,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::startTranslationRun", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 73, - "end_line": 81, + "start_line": 76, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -92340,8 +97614,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::stopTranslationRun", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 100, - "end_line": 103, + "start_line": 103, + "end_line": 106, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -92355,8 +97629,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::resetTranslationRun", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 110, - "end_line": 113, + "start_line": 113, + "end_line": 116, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -92370,8 +97644,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::updateTranslationRunState", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 119, - "end_line": 123, + "start_line": 122, + "end_line": 126, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -92385,8 +97659,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::pollStatus", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 127, - "end_line": 128, + "start_line": 130, + "end_line": 131, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -94141,7 +99415,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n

{page.status}

\n

\n {page.error?.message || \"Page not found\"}\n

\n \n Back to Dashboard\n \n
\n\n" + "body": "\n\n\n\n\n\n
\n

{page.status}

\n

\n {page.error?.message || \"Page not found\"}\n

\n \n Back to Dashboard\n \n
\n\n" }, { "contract_id": "RootLayout", @@ -94653,7 +99927,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n
\n \n \n \n \n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n
\n \n \n \n \n
\n\n\n" }, { "contract_id": "load:Function", @@ -95213,7 +100487,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n
\n

{$t.settings?.llm }

\n

\n {$t.settings?.llm_description ||\n \"Configure LLM providers for dashboard validation, documentation generation, and git assistance.\"}\n

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

\n {$t.settings?.llm_chatbot_settings_title }\n

\n

\n {$t.settings?.llm_chatbot_settings_description ||\n 'Select provider and optional model override for assistant intent planning.'}\n

\n\n
\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n
\n\n
\n \n \n
\n
\n
\n\n
\n

\n {$t.settings?.llm_provider_bindings_title }\n

\n

\n {$t.settings?.llm_provider_bindings_description ||\n 'Select which provider is used by default for each LLM task.'}\n

\n\n
\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n {#if bindings.dashboard_validation && !isMultimodalModel(getProviderById(bindings.dashboard_validation)?.default_model)}\n

\n {$t.settings?.llm_multimodal_warning ||\n 'Dashboard validation requires a multimodal model (image input).'}\n

\n {/if}\n
\n\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n
\n\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n
\n
\n
\n\n
\n

\n {$t.settings?.llm_prompts_title }\n

\n

\n {$t.settings?.llm_prompts_description ||\n 'Edit reusable prompts used for documentation, dashboard validation, and git commit generation.'}\n

\n\n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n\n
\n \n {savingPrompts ? '...' : ($t.settings?.save_llm_prompts )}\n \n
\n
\n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n
\n
\n

{$t.settings?.llm }

\n

\n {$t.settings?.llm_description ||\n \"Configure LLM providers for dashboard validation, documentation generation, and git assistance.\"}\n

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

\n {$t.settings?.llm_chatbot_settings_title }\n

\n

\n {$t.settings?.llm_chatbot_settings_description ||\n 'Select provider and optional model override for assistant intent planning.'}\n

\n\n
\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n
\n\n
\n \n \n
\n
\n
\n\n
\n

\n {$t.settings?.llm_provider_bindings_title }\n

\n

\n {$t.settings?.llm_provider_bindings_description ||\n 'Select which provider is used by default for each LLM task.'}\n

\n\n
\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n {#if bindings.dashboard_validation && !isMultimodalModel(getProviderById(bindings.dashboard_validation)?.default_model)}\n

\n {$t.settings?.llm_multimodal_warning ||\n 'Dashboard validation requires a multimodal model (image input).'}\n

\n {/if}\n
\n\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n
\n\n
\n \n \n \n {#each providers as provider}\n \n {/each}\n \n
\n
\n
\n\n
\n

\n {$t.settings?.llm_prompts_title }\n

\n

\n {$t.settings?.llm_prompts_description ||\n 'Edit reusable prompts used for documentation, dashboard validation, and git commit generation.'}\n

\n\n
\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n
\n\n
\n \n {savingPrompts ? '...' : ($t.settings?.save_llm_prompts )}\n \n
\n
\n {/if}\n
\n\n\n" }, { "contract_id": "handleSaveUser:Function", @@ -96505,10 +101779,23 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TYPE value '{any}' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{any}" + } }, { "code": "tag_forbidden_by_complexity", @@ -97096,69 +102383,212 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n
\n \n {$t.dataset_review?.workspace?.state_label}: {$t.dataset_review?.workspace?.state?.empty || \"Empty\"}\n \n {#if hasExistingSessions}\n \n {$t.dataset_review?.workspace_entry?.resume_available_badge}: {sessions.length}\n \n {/if}\n
\n
\n\n {#if isBootstrapping}\n
\n {$t.dataset_review?.workspace?.loading}\n
\n {:else}\n
\n
\n
\n
\n

\n {$t.dataset_review?.workspace_entry?.resume_eyebrow}\n

\n

\n {$t.dataset_review?.workspace_entry?.resume_title}\n

\n

\n {$t.dataset_review?.workspace_entry?.resume_description}\n

\n
\n \n {#if sessionsLoading}\n \n \n \n \n {$t.dataset_review?.workspace_entry?.resume_loading}\n {:else}\n \n \n \n {$t.common?.refresh}\n {/if}\n \n
\n \n
\n
\n \n {#if submitError}\n
\n {submitError}\n
\n {/if}\n
\n
\n {/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n
\n \n {$t.dataset_review?.workspace?.state_label}: {$t.dataset_review?.workspace?.state?.empty || \"Empty\"}\n \n {#if hasExistingSessions}\n \n {$t.dataset_review?.workspace_entry?.resume_available_badge}: {sessions.length}\n \n {/if}\n
\n
\n\n {#if isBootstrapping}\n
\n {$t.dataset_review?.workspace?.loading}\n
\n {:else}\n
\n
\n
\n
\n

\n {$t.dataset_review?.workspace_entry?.resume_eyebrow}\n

\n

\n {$t.dataset_review?.workspace_entry?.resume_title}\n

\n

\n {$t.dataset_review?.workspace_entry?.resume_description}\n

\n
\n \n {#if sessionsLoading}\n \n \n \n \n {$t.dataset_review?.workspace_entry?.resume_loading}\n {:else}\n \n \n \n {$t.common?.refresh}\n {/if}\n \n
\n \n
\n
\n \n {#if submitError}\n
\n {submitError}\n
\n {/if}\n
\n
\n {/if}\n
\n\n" + }, + { + "contract_id": "ReviewWorkspaceHeader", + "contract_type": "Component", + "file_path": "frontend/src/routes/datasets/review/ReviewWorkspaceHeader.svelte", + "start_line": 1, + "end_line": 42, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "LAYER": "UI", + "PURPOSE": "Header section for the dataset review workspace with title, description, and status badges." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n {#if session}\n \n {session.source_kind || $t.dataset_review?.workspace?.source_badge_fallback}\n \n \n {profile?.dataset_name || session.dataset_ref}\n \n {/if}\n \n {$t.dataset_review?.workspace?.state_label}: {getWorkspaceStateLabel(currentWorkspaceState)}\n \n {#if session}\n \n {$t.dataset_review?.workspace?.readiness_label}: {readinessLabel}\n \n {/if}\n
\n
\n\n" + }, + { + "contract_id": "ReviewWorkspaceLeftSidebar", + "contract_type": "Component", + "file_path": "frontend/src/routes/datasets/review/ReviewWorkspaceLeftSidebar.svelte", + "start_line": 1, + "end_line": 129, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "PURPOSE": "Left sidebar for dataset review workspace: source info, import status, session summary, clarification focus, primary actions." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n
\n

\n {$t.dataset_review?.workspace?.source_session_title}\n

\n\n
\n
\n {$t.dataset_review?.workspace?.source_label}\n
\n
\n {session.source_input || session.dataset_ref}\n
\n
\n\n
\n
\n
\n
\n {$t.dataset_review?.workspace?.import_status_title}\n
\n
\n {getWorkspaceStateLabel(currentWorkspaceState)}\n
\n
\n \n {readinessLabel}\n \n
\n\n
    \n {#each importMilestones as milestone}\n
  1. \n \n {milestone.label}\n
  2. \n {/each}\n
\n
\n\n
\n
\n {$t.dataset_review?.workspace?.session_label || \"Session summary\"}\n
\n
\n {profile?.dataset_name || session.dataset_ref}\n
\n
\n {profile?.business_summary_source || \"ai_draft\"} • {profile?.confidence_state || \"unresolved\"}\n
\n
\n \n {$t.dataset_review?.workspace?.filter_count_label || \"Filters\"}: {importedFilters.length}\n \n \n {$t.dataset_review?.workspace?.mapping_count_label || \"Mappings\"}: {executionMappings.length}\n \n \n {$t.dataset_review?.workspace?.finding_count_label || \"Findings\"}: {findings.length}\n \n
\n
\n\n {#if clarificationCurrentQuestion}\n
\n
\n {$t.dataset_review?.workspace?.clarification_focus_label || \"Clarification focus\"}\n
\n
{clarificationCurrentQuestion.question_text}
\n

{clarificationCurrentQuestion.why_it_matters}

\n onPromptAssistant(`clarification:${clarificationCurrentQuestion.question_id}`, clarificationCurrentQuestion.question_text)}\n >\n {$t.dataset_review?.clarification?.resume_action || \"Resume clarification\"}\n \n
\n {/if}\n\n
\n \n {getPrimaryActionCtaLabel()}\n \n \n {$t.dataset_review?.workspace?.save_session_action}\n \n
\n
\n\n" + }, + { + "contract_id": "ReviewWorkspaceRightRail", + "contract_type": "Component", + "file_path": "frontend/src/routes/datasets/review/ReviewWorkspaceRightRail.svelte", + "start_line": 1, + "end_line": 157, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "PURPOSE": "Right rail for dataset review workspace: next action, blockers, health counts, exports, SQL preview, launch panel." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n
\n
\n

\n {$t.dataset_review?.workspace?.next_action_card_eyebrow}\n

\n

\n {getRecommendedActionLabel(session.recommended_action)}\n

\n

\n {$t.dataset_review?.workspace?.execution_rail_body || \"The right rail stays dedicated to preview, blockers, exports, and launch.\"}\n

\n\n {#if workspaceLaunchBlockers.length > 0}\n
\n
\n {$t.dataset_review?.workspace?.launch_blockers_title || \"Launch blockers snapshot\"}\n
\n
    \n {#each workspaceLaunchBlockers as blocker}\n
  • {blocker}
  • \n {/each}\n
\n
\n {/if}\n
\n\n
\n

\n {$t.dataset_review?.workspace?.health_title}\n

\n
\n
\n
\n {$t.dataset_review?.findings?.blocking_title}\n
\n
{blockingCount}
\n
\n
\n
\n {$t.dataset_review?.findings?.warning_title}\n
\n
{warningCount}
\n
\n
\n
\n {$t.dataset_review?.findings?.informational_title}\n
\n
{infoCount}
\n
\n
\n
\n\n
\n

\n {$t.dataset_review?.workspace?.exports_title}\n

\n
\n onExportArtifact(\"documentation\", \"json\")}\n disabled={exportDisabled}\n >\n {$t.dataset_review?.workspace?.export_documentation_json}\n \n onExportArtifact(\"documentation\", \"markdown\")}\n disabled={exportDisabled}\n >\n {$t.dataset_review?.workspace?.export_documentation_markdown}\n \n onExportArtifact(\"validation\", \"json\")}\n disabled={exportDisabled}\n >\n {$t.dataset_review?.workspace?.export_validation_json}\n \n onExportArtifact(\"validation\", \"markdown\")}\n disabled={exportDisabled}\n >\n {$t.dataset_review?.workspace?.export_validation_markdown}\n \n
\n {#if exportMessage}\n
\n {exportMessage}\n
\n {/if}\n
\n\n
\n \n
\n\n
\n \n
\n
\n\n" }, { "contract_id": "DatasetReviewWorkspace", "contract_type": "Page", "file_path": "frontend/src/routes/datasets/review/[id]/+page.svelte", "start_line": 1, - "end_line": 1202, + "end_line": 314, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, + "DEPRECATED": "N/A — active workspace page.", "LAYER": "Page", - "POST": "Session state is loaded and displayed in the workspace; user can review, clarify, and launch.", - "PRE": "Route requires valid session ID in URL params; environment context and API client must be initialized.", - "PURPOSE": "Main dataset review workspace coordinating session state through a three-column chat-centric layout where the assistant is the primary review surface.", - "SIDE_EFFECT": "Fetches session data from the API on mount; updates datasetReviewSessionStore.", - "UX_FEEDBACK": "Main CTA reflects current highest-value next action.", - "UX_RECOVERY": "Users can save, resume, or reopen unfinished sessions without losing context.", - "UX_STATE": "Ready -> Preview, blockers, and launch action visible." + "PURPOSE": "Main dataset review workspace — thin shell delegating to extracted components.", + "RATIONALE": "Decomposed from 1202→~380 lines by extracting helpers, composables, and sidebar/rail/header components per INV_7.", + "REJECTED": "Keeping all logic inline was rejected — exceeded 150-line contract limit and cyclomatic complexity thresholds.", + "REPLACED_BY": "N/A", + "UX_STATE": "Review -> 3-column layout." }, "relations": [ { "source_id": "DatasetReviewWorkspace", "relation_type": "DEPENDS_ON", - "target_id": "SourceIntakePanel", - "target_ref": "[SourceIntakePanel]" + "target_id": "ReviewWorkspaceHeader", + "target_ref": "[ReviewWorkspaceHeader]" }, { "source_id": "DatasetReviewWorkspace", "relation_type": "DEPENDS_ON", - "target_id": "CompiledSQLPreview", - "target_ref": "[CompiledSQLPreview]" + "target_id": "ReviewWorkspaceLeftSidebar", + "target_ref": "[ReviewWorkspaceLeftSidebar]" }, { "source_id": "DatasetReviewWorkspace", "relation_type": "DEPENDS_ON", - "target_id": "LaunchConfirmationPanel", - "target_ref": "[LaunchConfirmationPanel]" + "target_id": "ReviewWorkspaceRightRail", + "target_ref": "[ReviewWorkspaceRightRail]" }, { "source_id": "DatasetReviewWorkspace", "relation_type": "DEPENDS_ON", - "target_id": "ValidationFindingsPanel", - "target_ref": "[ValidationFindingsPanel]" - }, - { - "source_id": "DatasetReviewWorkspace", - "relation_type": "DEPENDS_ON", - "target_id": "SemanticLayerReview", - "target_ref": "[SemanticLayerReview]" - }, - { - "source_id": "DatasetReviewWorkspace", - "relation_type": "DEPENDS_ON", - "target_id": "ExecutionMappingReview", - "target_ref": "[ExecutionMappingReview]" - }, - { - "source_id": "DatasetReviewWorkspace", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantChatPanel", - "target_ref": "[AssistantChatPanel]" + "target_id": "UseReviewSession", + "target_ref": "[UseReviewSession]" }, { "source_id": "DatasetReviewWorkspace", @@ -97166,23 +102596,11 @@ "target_id": "datasetReviewSessionStore", "target_ref": "[datasetReviewSessionStore]" }, - { - "source_id": "DatasetReviewWorkspace", - "relation_type": "BINDS_TO", - "target_id": "environmentContextStore", - "target_ref": "[environmentContextStore]" - }, { "source_id": "DatasetReviewWorkspace", "relation_type": "BINDS_TO", "target_id": "assistantChatStore", "target_ref": "[assistantChatStore]" - }, - { - "source_id": "DatasetReviewWorkspace", - "relation_type": "CALLS", - "target_id": "api_module", - "target_ref": "[api_module]" } ], "schema_warnings": [ @@ -97212,6 +102630,33 @@ "contract_type": "Page" } }, + { + "code": "tag_not_for_contract_type", + "tag": "DEPRECATED", + "message": "@DEPRECATED is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "DEPRECATED", + "message": "@DEPRECATED is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, { "code": "tag_not_for_contract_type", "tag": "LAYER", @@ -97251,54 +102696,6 @@ "value": "Page" } }, - { - "code": "tag_not_for_contract_type", - "tag": "POST", - "message": "@POST is not allowed for contract type 'Page'", - "detail": { - "actual_type": "Page", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Page' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Page" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PRE", - "message": "@PRE is not allowed for contract type 'Page'", - "detail": { - "actual_type": "Page", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Page' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Page" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -97328,8 +102725,8 @@ }, { "code": "tag_not_for_contract_type", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is not allowed for contract type 'Page'", + "tag": "RATIONALE", + "message": "@RATIONALE is not allowed for contract type 'Page'", "detail": { "actual_type": "Page", "allowed_types": [ @@ -97337,14 +102734,17 @@ "Function", "Class", "Component", + "Block", + "ADR", + "Skill", "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Page' at C4", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Page' at C4", "detail": { "actual_complexity": 4, "contract_type": "Page" @@ -97352,19 +102752,26 @@ }, { "code": "tag_not_for_contract_type", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "tag": "REJECTED", + "message": "@REJECTED is not allowed for contract type 'Page'", "detail": { "actual_type": "Page", "allowed_types": [ - "Component" + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C4", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Page' at C4", "detail": { "actual_complexity": 4, "contract_type": "Page" @@ -97372,19 +102779,26 @@ }, { "code": "tag_not_for_contract_type", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "tag": "REPLACED_BY", + "message": "@REPLACED_BY is not allowed for contract type 'Page'", "detail": { "actual_type": "Page", "allowed_types": [ - "Component" + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C4", + "tag": "REPLACED_BY", + "message": "@REPLACED_BY is forbidden for contract type 'Page' at C4", "detail": { "actual_complexity": 4, "contract_type": "Page" @@ -97422,7 +102836,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n {#if session}\n \n {session.source_kind || $t.dataset_review?.workspace?.source_badge_fallback}\n \n \n {profile?.dataset_name || session.dataset_ref}\n \n {/if}\n \n {$t.dataset_review?.workspace?.state_label}: {getWorkspaceStateLabel(currentWorkspaceState)}\n \n {#if session}\n \n {$t.dataset_review?.workspace?.readiness_label}: {readinessLabel}\n \n {/if}\n
\n
\n\n {#if isBootstrapping}\n
\n {$t.dataset_review?.workspace?.loading}\n
\n {:else}\n {#if loadError}\n
\n {loadError}\n
\n {/if}\n\n {#if !session}\n \n\n {#if currentWorkspaceState === \"importing\"}\n
\n
\n
\n

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

\n

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

\n

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

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

\n AI orchestration\n

\n

\n {profile?.dataset_name || session.dataset_ref}\n

\n

\n {profile?.business_summary || assistantSeedPrompt}\n

\n
\n
\n
\n {$t.dataset_review?.workspace?.next_action_label}\n
\n
\n {getRecommendedActionLabel(session.recommended_action)}\n
\n
\n
\n\n
\n \n handleAskAiContext({\n target: \"summary\",\n section: \"summary\",\n label: profile?.dataset_name || session.dataset_ref,\n prompt: profile?.business_summary || assistantSeedPrompt,\n })}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n \n handleAskAiContext({\n target: \"summary\",\n section: \"summary\",\n label: profile?.dataset_name || session.dataset_ref,\n prompt: profile?.business_summary || assistantSeedPrompt,\n mode: \"improve\",\n })}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n
\n\n {#if session.readiness_state === \"recovery_required\" || session.readiness_state === \"partially_ready\"}\n
\n {$t.dataset_review?.workspace?.partial_recovery_notice || \"The session is in partial recovery mode. Review and assistant guidance stay available, but launch remains blocked until blockers are resolved and a current SQL preview is ready.\"}\n
\n {/if}\n\n {#if importedFilters.length > 0}\n
\n
\n
\n {$t.dataset_review?.workspace?.recovered_filters_title || \"Recovered filters\"}\n
\n \n {importedFilters.length}\n \n
\n
\n {#each importedFilters.slice(0, 4) as filter}\n \n handleAskAiContext({\n target: `filter:${filter.filter_id}`,\n section: \"filters\",\n label: filter.display_name || filter.filter_name,\n prompt: `${filter.display_name || filter.filter_name}: ${stringifyValue(filter.normalized_value ?? filter.raw_value)}`,\n })}\n >\n {filter.display_name || filter.filter_name}\n {stringifyValue(filter.normalized_value ?? filter.raw_value)}\n \n {/each}\n {#if importedFilters.length > 4}\n \n +{importedFilters.length - 4}\n \n {/if}\n
\n
\n {/if}\n
\n\n
\n \n
\n\n \n\n
\n \n
\n\n
\n \n
\n
\n\n \n
\n {/if}\n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n getWorkspaceStateLabel($t, s)} {profile} />\n\n {#if isBootstrapping}\n
{$t.dataset_review?.workspace?.loading}
\n {:else}\n {#if loadError}\n
{loadError}
\n {/if}\n\n {#if !session}\n \n {#if currentWorkspaceState === \"importing\"}\n
\n
\n
\n

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

\n

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

\n

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

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

AI orchestration

\n

{profile?.dataset_name || session.dataset_ref}

\n

{profile?.business_summary || assistantSeedPrompt}

\n
\n
\n
{$t.dataset_review?.workspace?.next_action_label}
\n
{getRecommendedActionLabel($t, session.recommended_action)}
\n
\n
\n
\n \n \n
\n {#if session.readiness_state === \"recovery_required\" || session.readiness_state === \"partially_ready\"}\n
{$t.dataset_review?.workspace?.partial_recovery_notice || \"Partial recovery mode.\"}
\n {/if}\n {#if importedFilters.length > 0}\n
\n
\n
{$t.dataset_review?.workspace?.recovered_filters_title || \"Recovered filters\"}
\n {importedFilters.length}\n
\n
\n {#each importedFilters.slice(0, 4) as filter}\n \n {/each}\n {#if importedFilters.length > 4}\n +{importedFilters.length - 4}\n {/if}\n
\n
\n {/if}\n
\n\n
\n\n \n\n
\n\n
\n
\n\n \n
\n {/if}\n {/if}\n
\n\n" }, { "contract_id": "DatasetReviewEntryUxTests", @@ -97511,6 +102925,203 @@ "tier_source": "AutoCalculated", "body": "// [DEF:DatasetReviewEntryUxTests:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, workspace-entry, resume, source-intake, ux-tests\n// @PURPOSE: Verify dataset review entry route exposes resumable sessions alongside the new session intake flow.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [DatasetReviewWorkspaceEntry]\n// @UX_STATE: Loading -> workspace loader is shown before bootstrap completes.\n// @UX_STATE: ResumeList -> existing sessions render with summary fields and resume links.\n// @UX_STATE: ResumeEmpty -> empty-state copy is shown when no sessions are returned.\n// @UX_STATE: ResumeError -> inline error and retry action remain visible without removing new session flow.\n// @TEST_SCENARIO: renders_resume_sessions_and_new_session_intake\n// @TEST_SCENARIO: renders_empty_resume_state_when_no_sessions_exist\n// @TEST_SCENARIO: renders_resume_error_and_retry\n// @TEST_SCENARIO: submits_new_session_without_regression\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport DatasetReviewEntryPage from \"../+page.svelte\";\nimport { api } from \"$lib/api.js\";\n\nconst mockedGoto = vi.mocked(await import(\"$app/navigation\")).goto;\n\nfunction createSessionSummary(overrides = {}) {\n return {\n session_id: \"session-1\",\n user_id: \"user-1\",\n environment_id: \"env-1\",\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n dataset_ref: \"public.sales\",\n dataset_id: 101,\n readiness_state: \"partially_ready\",\n recommended_action: \"resume_session\",\n status: \"paused\",\n current_phase: \"review\",\n created_at: \"2026-03-18T06:00:00.000Z\",\n updated_at: \"2026-03-18T06:10:00.000Z\",\n last_activity_at: \"2026-03-18T06:15:00.000Z\",\n ...overrides,\n };\n}\n\nvi.mock(\"$app/navigation\", () => ({\n goto: vi.fn(),\n}));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n refresh: \"Refresh\",\n retry: \"Retry\",\n choose_environment: \"Choose environment\",\n not_available: \"N/A\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n },\n workspace_entry: {\n resume_eyebrow: \"Resume session\",\n resume_title: \"Continue an existing review session\",\n resume_description:\n \"If a dataset review was already started, open the relevant session and continue from its latest saved state.\",\n resume_available_badge: \"Sessions available\",\n resume_loading: \"Loading existing sessions...\",\n resume_load_failed: \"Failed to load existing review sessions.\",\n resume_empty_title: \"No saved sessions yet\",\n resume_empty_body:\n \"Start a new review session from the source intake panel to make it available here for reopening later.\",\n resume_action: \"Resume\",\n session_id_label: \"Session ID\",\n dataset_ref_label: \"Dataset reference\",\n environment_id_label: \"Environment\",\n status_label: \"Status\",\n readiness_label: \"Readiness\",\n phase_label: \"Phase\",\n updated_at_label: \"Updated\",\n last_activity_at_label: \"Last activity\",\n },\n workspace: {\n eyebrow: \"Dataset review\",\n title: \"Dataset review workspace\",\n description: \"Review imported dataset context.\",\n state_label: \"Workspace state\",\n loading: \"Loading workspace\",\n state: {\n empty: \"Empty\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n fetchApi: vi.fn(),\n postApi: vi.fn(),\n requestApi: vi.fn(),\n },\n}));\n\nvi.mock(\"$lib/stores/environmentContext.js\", () => ({\n environmentContextStore: {\n subscribe: (run) => {\n run({\n environments: [{ id: \"env-1\", name: \"DEV\" }],\n selectedEnvId: \"env-1\",\n });\n return () => {};\n },\n },\n initializeEnvironmentContext: vi.fn().mockResolvedValue(undefined),\n}));\n\ndescribe(\"DatasetReviewWorkspaceEntry UX Contract\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockReset();\n api.postApi.mockReset();\n mockedGoto.mockReset();\n });\n\n it(\"renders_resume_sessions_and_new_session_intake\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [\n createSessionSummary(),\n createSessionSummary({\n session_id: \"session-2\",\n dataset_ref: \"analytics.daily_margin\",\n environment_id: \"env-2\",\n status: \"active\",\n readiness_state: \"review_ready\",\n current_phase: \"semantic_review\",\n }),\n ],\n total: 2,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions?page=1&page_size=5\",\n );\n });\n\n // Page header\n expect(screen.getByText(\"Dataset review\")).toBeDefined();\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Review imported dataset context.\")).toBeDefined();\n\n // Resume section header\n expect(screen.getByText(\"Continue an existing review session\")).toBeDefined();\n\n // Source intake section\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_empty_resume_state_when_no_sessions_exist\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [],\n total: 0,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalled();\n });\n\n // Page header renders regardless\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_resume_error_and_retry\", async () => {\n api.fetchApi.mockRejectedValueOnce(new Error(\"Session list failed\"));\n api.fetchApi.mockResolvedValueOnce({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n // Wait for initial fetch to complete (error is swallowed, but retry is possible via refresh button)\n await waitFor(() => {\n // After bootstrap, source intake should render\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n // Click the refresh button to retry session loading\n const refreshButton = screen.getByRole(\"button\", { name: \"Refresh\" });\n await fireEvent.click(refreshButton);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledTimes(2);\n });\n\n // Source intake still visible after retry\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"submits_new_session_without_regression\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n api.postApi.mockResolvedValue({ session_id: \"created-session\" });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n const environmentSelect = screen.getByRole(\"combobox\");\n const sourceInput = screen.getByPlaceholderText(\"https://superset.local/dashboard/10\");\n const submitButton = screen.getByRole(\"button\", { name: \"Start from link\" });\n\n await fireEvent.change(environmentSelect, { target: { value: \"env-1\" } });\n await fireEvent.input(sourceInput, {\n target: { value: \"https://superset.local/dashboard/10\" },\n });\n await fireEvent.click(submitButton);\n\n await waitFor(() => {\n expect(api.postApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions\", {\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n environment_id: \"env-1\",\n });\n });\n\n expect(mockedGoto).toHaveBeenCalledWith(\"/datasets/review/created-session\");\n });\n});\n\n// [/DEF:DatasetReviewEntryUxTests:Module]\n" }, + { + "contract_id": "ReviewWorkspaceHelpers", + "contract_type": "Module", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 1, + "end_line": 120, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Shared helper functions for the dataset review workspace.", + "RATIONALE": "Extracted from DatasetReviewWorkspace to reduce page script size per INV_7." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region ReviewWorkspaceHelpers [C:2] [TYPE Module] [SEMANTICS dataset-review, helpers, derived, utils]\n// @BRIEF Shared helper functions for the dataset review workspace.\n// @RATIONALE Extracted from DatasetReviewWorkspace to reduce page script size per INV_7.\n\n/**\n * Build import milestones for the workspace progress tracking.\n */\nexport function buildImportMilestones(t, intakeAcknowledgment, session, importedFilters, latestPreview, profile, findings, currentWorkspaceState) {\n const recognizedDone = Boolean(intakeAcknowledgment || session?.dataset_ref);\n const filtersDone = importedFilters.length > 0;\n const variablesDone = Boolean(importedFilters.length || latestPreview);\n const semanticsDone = Boolean(profile?.business_summary || findings.length > 0);\n const importState = currentWorkspaceState === \"importing\";\n\n return [\n { key: \"recognized\", label: t.dataset_review?.workspace?.import_milestones?.recognized, state: recognizedDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"filters\", label: t.dataset_review?.workspace?.import_milestones?.filters, state: filtersDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"variables\", label: t.dataset_review?.workspace?.import_milestones?.variables, state: variablesDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"semantic_sources\", label: t.dataset_review?.workspace?.import_milestones?.semantic_sources, state: semanticsDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"summary\", label: t.dataset_review?.workspace?.import_milestones?.summary, state: semanticsDone ? \"done\" : importState ? \"active\" : \"pending\" },\n ];\n}\n\n/**\n * Build workspace launch blockers list.\n */\nexport function buildWorkspaceLaunchBlockers(t, session, unresolvedBlockingFindingsCount, pendingApprovalMappingsCount, previewGateState) {\n if (!session) return [];\n const blockers = [];\n if (unresolvedBlockingFindingsCount > 0)\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.blocking_findings || \"Blocking findings\"}: ${unresolvedBlockingFindingsCount}`);\n if (pendingApprovalMappingsCount > 0)\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.mapping_approvals || \"Mapping approvals\"}: ${pendingApprovalMappingsCount}`);\n if (previewGateState !== \"ready\")\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.preview_state || \"SQL preview state\"}: ${previewGateState}`);\n if (String(session.readiness_state || \"\") !== \"run_ready\")\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.readiness || \"Readiness\"}: ${session.readiness_state || \"unknown\"}`);\n return blockers;\n}\n\n/**\n * Build assistant seed prompt from session context.\n */\nexport function buildAssistantSeedPrompt(t, session, profile, workspaceLaunchBlockers, clarificationCurrentQuestion, importedFilters) {\n if (!session) return \"\";\n const intro = [\n `Session ${session.session_id}`,\n `Dataset ${profile?.dataset_name || session.dataset_ref}`,\n `Phase ${session.current_phase || \"review\"}`,\n `Readiness ${session.readiness_state || \"unknown\"}`,\n ];\n if (workspaceLaunchBlockers.length > 0)\n intro.push(`Launch blockers: ${workspaceLaunchBlockers.join(\"; \")}`);\n if (clarificationCurrentQuestion?.question_text)\n intro.push(`Active clarification: ${clarificationCurrentQuestion.question_text}`);\n else if (importedFilters.length > 0)\n intro.push(`Recovered filters: ${importedFilters.length}`);\n return intro.join(\". \");\n}\n\n/**\n * Build assistant context prompt for contextual queries.\n */\nexport function buildAssistantContextPrompt(t, payload) {\n const label = payload?.label || payload?.target || \"current item\";\n const section = payload?.section || \"workspace\";\n const context = String(payload?.prompt || \"\").trim();\n if (payload?.mode === \"improve\") {\n return `${t.dataset_review?.workspace?.assistant_context?.improve_prefix || \"Improve\"} ${label}. ${t.dataset_review?.workspace?.assistant_context?.section_label || \"Section\"}: ${section}.${context ? ` ${t.dataset_review?.workspace?.assistant_context?.context_label || \"Context\"}: ${context}` : \"\"}`;\n }\n return `${t.dataset_review?.workspace?.assistant_context?.ask_prefix || \"Help review\"} ${label}. ${t.dataset_review?.workspace?.assistant_context?.section_label || \"Section\"}: ${section}.${context ? ` ${t.dataset_review?.workspace?.assistant_context?.context_label || \"Context\"}: ${context}` : \"\"}`;\n}\n\n/**\n * Get workspace state label from i18n.\n */\nexport function getWorkspaceStateLabel(t, state) {\n return t.dataset_review?.workspace?.state?.[state] || state;\n}\n\n/**\n * Get recommended action label from i18n.\n */\nexport function getRecommendedActionLabel(t, action) {\n const normalized = String(action || \"import_from_superset\");\n return t.dataset_review?.workspace?.actions?.[normalized] || normalized;\n}\n\n/**\n * Get primary action CTA label.\n */\nexport function getPrimaryActionCtaLabel(t, session) {\n if (!session) return t.dataset_review?.workspace?.next_action_label;\n if (session.recommended_action === \"resume_session\") return t.dataset_review?.workspace?.resume_action;\n if (session.recommended_action === \"generate_sql_preview\") return t.dataset_review?.preview?.generate_action;\n if (session.recommended_action === \"launch_dataset\") return t.dataset_review?.launch?.launch_action;\n return t.dataset_review?.workspace?.next_action_label;\n}\n\n/**\n * Merge a single item into a collection by id key, replacing if exists.\n */\nexport function mergeCollectionItem(collection = [], item = null, idKey = \"id\") {\n if (!item) return collection;\n const itemId = item?.[idKey];\n if (!itemId) return collection;\n return collection.some((entry) => entry?.[idKey] === itemId)\n ? collection.map((entry) => (entry?.[idKey] === itemId ? { ...entry, ...item } : entry))\n : [...collection, item];\n}\n\n/**\n * Stringify a value for display.\n */\nexport function stringifyValue(t, value) {\n if (value === null || value === undefined || value === \"\") return t.common?.not_available || \"N/A\";\n if (typeof value === \"string\") return value;\n try { return JSON.stringify(value); } catch (_) { return String(value); }\n}\n// #endregion ReviewWorkspaceHelpers\n" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::buildImportMilestones", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 5, + "end_line": 8, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Build import milestones for the workspace progress tracking.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::buildWorkspaceLaunchBlockers", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 24, + "end_line": 27, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Build workspace launch blockers list.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::buildAssistantSeedPrompt", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 41, + "end_line": 44, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Build assistant seed prompt from session context.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::buildAssistantContextPrompt", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 61, + "end_line": 64, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Build assistant context prompt for contextual queries.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::getWorkspaceStateLabel", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 74, + "end_line": 77, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Get workspace state label from i18n.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::getRecommendedActionLabel", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 81, + "end_line": 84, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Get recommended action label from i18n.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::getPrimaryActionCtaLabel", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 89, + "end_line": 92, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Get primary action CTA label.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::mergeCollectionItem", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 100, + "end_line": 103, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Merge a single item into a collection by id key, replacing if exists.\n */" + }, + { + "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::stringifyValue", + "contract_type": "Function", + "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", + "start_line": 112, + "end_line": 115, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Stringify a value for display.\n */" + }, + { + "contract_id": "UseReviewSession", + "contract_type": "Module", + "file_path": "frontend/src/routes/datasets/review/useReviewSession.js", + "start_line": 1, + "end_line": 273, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Composable for dataset review session state management — load, update, export, and clarify." + }, + "relations": [ + { + "source_id": "UseReviewSession", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + }, + { + "source_id": "UseReviewSession", + "relation_type": "DEPENDS_ON", + "target_id": "DatasetReviewApi", + "target_ref": "[DatasetReviewApi]" + }, + { + "source_id": "UseReviewSession", + "relation_type": "BINDS_TO", + "target_id": "datasetReviewSessionStore", + "target_ref": "[datasetReviewSessionStore]" + }, + { + "source_id": "UseReviewSession", + "relation_type": "BINDS_TO", + "target_id": "assistantChatStore", + "target_ref": "[assistantChatStore]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region UseReviewSession [C:3] [TYPE Module] [SEMANTICS dataset-review, session, api, handlers]\n// @BRIEF Composable for dataset review session state management — load, update, export, and clarify.\n// @RELATION DEPENDS_ON -> [ApiModule]\n// @RELATION DEPENDS_ON -> [DatasetReviewApi]\n// @RELATION BINDS_TO -> [datasetReviewSessionStore]\n// @RELATION BINDS_TO -> [assistantChatStore]\nimport { goto } from \"$app/navigation\";\nimport { api } from \"$lib/api.js\";\nimport {\n extractDatasetReviewVersion,\n getDatasetReviewConflictMessage,\n isDatasetReviewConflictError,\n normalizeDatasetReviewDetail,\n requestDatasetReviewApi,\n} from \"$lib/api/datasetReview.js\";\nimport {\n datasetReviewSessionStore,\n setLoading,\n setError,\n setSession,\n resetSession,\n} from \"$lib/stores/datasetReviewSession.js\";\nimport {\n assistantChatStore,\n setAssistantDatasetReviewSessionId,\n setAssistantSeedMessage,\n setAssistantFocusTarget,\n} from \"$lib/stores/assistantChat.js\";\n\nexport function useReviewSession() {\n return {\n loadSessionDetail,\n loadClarificationState,\n handleSourceSubmit,\n handleResumeSession,\n updateSessionLifecycle,\n exportArtifact,\n handlePreviewUpdated,\n handleSemanticUpdated,\n handleMappingUpdated,\n handleLaunchUpdated,\n handleSectionJump,\n handleAskAiContext,\n primaryActionHandler,\n buildSessionUrl,\n };\n}\n\nfunction buildSessionUrl(sessionId) {\n return `/datasets/review/${encodeURIComponent(String(sessionId))}`;\n}\n\nasync function loadSessionDetail(sessionId) {\n if (!sessionId) {\n return { session: null, clarificationState: null, loadError: \"\" };\n }\n\n setLoading(true);\n let sessionResult = null;\n let clarificationResult = null;\n let error = \"\";\n let previewUi = \"\";\n\n try {\n const detail = await api.fetchApi(`/dataset-orchestration/sessions/${sessionId}`);\n sessionResult = normalizeDatasetReviewDetail(detail);\n previewUi = \"\";\n setSession(sessionResult);\n setAssistantDatasetReviewSessionId(sessionResult?.session_id || null);\n clarificationResult = await loadClarificationStateInline(sessionResult);\n } catch (err) {\n error = err?.message || \"Failed to load session\";\n setError(error);\n } finally {\n setLoading(false);\n }\n\n return { session: sessionResult, clarificationState: clarificationResult, loadError: error, previewUiState: previewUi };\n}\n\nasync function loadClarificationStateInline(sessionDetail) {\n if (!sessionDetail?.session_id || !sessionDetail?.clarification_sessions?.length) return null;\n try {\n return await api.fetchApi(\n `/dataset-orchestration/sessions/${sessionDetail.session_id}/clarification`,\n );\n } catch (_error) {\n return null;\n }\n}\n\n// Re-export for external use\nconst loadClarificationState = loadClarificationStateInline;\n\nasync function handleSourceSubmit(session, intakeAcknowledgment, submitError, loadError, isSubmitting) {\n // This is handled inline in the page — returns a operation object\n return { intakeAcknowledgment, submitError, loadError, isSubmitting };\n}\n\nasync function handleResumeSession(sessionId, sessionVersion) {\n if (!sessionId) return { loadError: \"\" };\n try {\n const updated = await requestDatasetReviewApi(\n `/dataset-orchestration/sessions/${sessionId}`, \"PATCH\",\n { status: \"active\" }, sessionVersion,\n );\n await goto(buildSessionUrl(updated.session_id));\n return { loadError: \"\" };\n } catch (error) {\n const msg = (isDatasetReviewConflictError(error)\n ? getDatasetReviewConflictMessage(error)\n : error?.message) || \"Failed to resume session\";\n return { loadError: msg };\n }\n}\n\nasync function updateSessionLifecycle(sessionId, statusValue, sessionVersion) {\n if (!sessionId) return { loadError: \"\" };\n try {\n await requestDatasetReviewApi(\n `/dataset-orchestration/sessions/${sessionId}`, \"PATCH\",\n { status: statusValue }, sessionVersion,\n );\n return { loadError: \"\" };\n } catch (error) {\n const msg = (isDatasetReviewConflictError(error)\n ? getDatasetReviewConflictMessage(error)\n : error?.message) || \"Failed to save session\";\n return { loadError: msg };\n }\n}\n\nasync function exportArtifact(sessionId, kind, format) {\n if (!sessionId) return { exportMessage: \"\", isExporting: false };\n try {\n const artifact = await api.fetchApi(\n `/dataset-orchestration/sessions/${sessionId}/exports/${kind}?format=${format}`,\n );\n return { exportMessage: `${artifact.artifact_type} • ${artifact.format} • ${artifact.storage_ref}`, isExporting: false };\n } catch (error) {\n return { exportMessage: error?.message || \"Export failed\", isExporting: false };\n }\n}\n\nfunction handlePreviewUpdated(session, result) {\n if (!session || !result?.preview) return { session: null, previewUiState: \"\" };\n const previewUiState = result.preview_state || result.preview.preview_status || \"\";\n const existingPreviews = session.previews || [];\n const nextPreviews = existingPreviews.some(\n (item) => item.preview_id === result.preview.preview_id,\n )\n ? existingPreviews.map((item) =>\n item.preview_id === result.preview.preview_id ? result.preview : item,\n )\n : [...existingPreviews, result.preview];\n return {\n session: {\n ...session,\n session_version: extractDatasetReviewVersion(result.preview) ?? session.session_version,\n version: extractDatasetReviewVersion(result.preview) ?? session.version,\n previews: nextPreviews,\n preview: result.preview,\n readiness_state:\n result.preview.preview_status === \"ready\"\n ? session.readiness_state === \"run_ready\" ? \"run_ready\" : \"compiled_preview_ready\"\n : session.readiness_state,\n },\n previewUiState,\n };\n}\n\nfunction handleSemanticUpdated(session, result) {\n if (!session || !result) return { session: null };\n const nextVersion = extractDatasetReviewVersion(result);\n let updatedSession = { ...session };\n if (Array.isArray(result?.fields)) {\n updatedSession.semantic_fields = result.fields;\n updatedSession = applySessionVersionInline(updatedSession, nextVersion);\n } else if (result.field_id) {\n updatedSession.semantic_fields = mergeCollectionItemInline(updatedSession.semantic_fields || [], result, \"field_id\");\n updatedSession = applySessionVersionInline(updatedSession, nextVersion);\n }\n return { session: updatedSession };\n}\n\nfunction handleMappingUpdated(session, result) {\n if (!session || !result) return { session: null, previewUiState: \"\" };\n const nextVersion = extractDatasetReviewVersion(result);\n let updatedSession = { ...session };\n let previewUiState = \"\";\n if (Array.isArray(result?.mappings)) {\n updatedSession.execution_mappings = result.mappings;\n updatedSession = applySessionVersionInline(updatedSession, nextVersion);\n } else if (result.mapping_id || result.mapping) {\n const mapping = result.mapping || result;\n updatedSession.execution_mappings = mergeCollectionItemInline(updatedSession.execution_mappings || [], mapping, \"mapping_id\");\n updatedSession = applySessionVersionInline(updatedSession, nextVersion);\n }\n if (result.preview_state) previewUiState = result.preview_state;\n return { session: updatedSession, previewUiState };\n}\n\nfunction handleLaunchUpdated(result) {\n return { launchResult: result?.launch_result || result || null };\n}\n\nfunction handleSectionJump(target) {\n const normalizedTarget = {\n preview: \"sql-preview\", \"sql-preview\": \"sql-preview\",\n mappings: \"mapping\", mapping: \"mapping\",\n semantics: \"semantics\", summary: \"summary\",\n clarification: \"assistant-chat\", filters: \"summary\",\n intake: \"summary\", \"next-action\": \"next-action\",\n }[target] || target;\n document.getElementById(normalizedTarget)?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n}\n\nfunction handleAskAiContext(payload, setAssistantFocusTargetFn, setAssistantSeedMessageFn) {\n if (!payload) return;\n if (payload.target) {\n setAssistantFocusTargetFn({ target: payload.target, source: \"workspace_context_action\" });\n }\n setAssistantSeedMessageFn(buildAssistantContextPromptInline(payload));\n document.getElementById(\"assistant-chat\")?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n}\n\nfunction primaryActionHandler(session, clarificationCurrentQuestion, assistantSeedPrompt, handleResumeSessionFn, handleAskAiContextFn) {\n if (!session) return;\n if (session.recommended_action === \"resume_session\") { handleResumeSessionFn(); return; }\n if (session.recommended_action === \"generate_sql_preview\") {\n handleAskAiContextFn({\n target: \"sql-preview\", section: \"sql-preview\",\n label: \"sql-preview\",\n prompt: \"Explain which approvals or fixes are still required before regenerating SQL preview.\",\n });\n document.getElementById(\"sql-preview\")?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n return;\n }\n if (session.recommended_action === \"launch_dataset\") {\n document.getElementById(\"launch\")?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n return;\n }\n handleAskAiContextFn({\n target: clarificationCurrentQuestion?.question_id ? `clarification:${clarificationCurrentQuestion.question_id}` : \"clarification\",\n section: \"clarification\",\n prompt: assistantSeedPrompt,\n });\n}\n\nfunction buildAssistantContextPromptInline(payload) {\n const label = payload?.label || payload?.target || \"current item\";\n const section = payload?.section || \"workspace\";\n const context = String(payload?.prompt || \"\").trim();\n if (payload?.mode === \"improve\") {\n return `Improve ${label}. Section: ${section}.${context ? ` Context: ${context}` : \"\"}`;\n }\n return `Help review ${label}. Section: ${section}.${context ? ` Context: ${context}` : \"\"}`;\n}\n\nfunction applySessionVersionInline(sessionObj, nextVersion) {\n if (nextVersion === null || nextVersion === undefined) return sessionObj;\n return { ...sessionObj, session_version: nextVersion, version: nextVersion };\n}\n\nfunction mergeCollectionItemInline(collection = [], item = null, idKey = \"id\") {\n if (!item) return collection;\n const itemId = item?.[idKey];\n if (!itemId) return collection;\n return collection.some((entry) => entry?.[idKey] === itemId)\n ? collection.map((entry) => (entry?.[idKey] === itemId ? { ...entry, ...item } : entry))\n : [...collection, item];\n}\n// #endregion UseReviewSession\n" + }, { "contract_id": "GitDashboardPage", "contract_type": "Page", @@ -97930,7 +103541,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n

{$t.auth?.login }

\n\n {#if error}\n
\n {error}\n
\n {/if}\n\n
{ event.preventDefault(); handleLogin(); }} class=\"space-y-4\">\n
\n \n \n
\n\n
\n \n \n
\n\n \n {loading ? ($t.auth?.logging_in ) : ($t.auth?.login )}\n \n \n\n
\n
\n
\n
\n
\n
\n {$t.auth?.or_continue_with }\n
\n
\n\n
\n \n {$t.auth?.corporate_sso }\n \n
\n
\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n

{$t.auth?.login }

\n\n {#if error}\n
\n {error}\n
\n {/if}\n\n
{ event.preventDefault(); handleLogin(); }} class=\"space-y-4\">\n
\n \n \n
\n\n
\n \n \n
\n\n \n {loading ? ($t.auth?.logging_in ) : ($t.auth?.login )}\n \n \n\n
\n
\n
\n
\n
\n
\n {$t.auth?.or_continue_with }\n
\n
\n\n
\n \n {$t.auth?.corporate_sso }\n \n
\n
\n
\n\n\n" }, { "contract_id": "ReactiveDashboardFetch:Block", @@ -99862,7 +105473,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n null}\n actions={() => null}\n />\n\n
\n
\n \n {#each TASK_TYPE_OPTIONS as option}\n \n {/each}\n \n\n \n {#each STATUS_OPTIONS as option}\n \n {/each}\n \n\n loadReports()}\n >\n {$t.common?.refresh}\n \n\n \n {$t.reports?.clear_filters}\n \n
\n
\n\n {#if loading}\n \n {$t.reports?.loading}\n
\n {:else if error}\n \n

{error}

\n loadReports()}\n >\n {$t.reports?.retry_load || $t.common?.retry}\n \n
\n {:else if !collection || collection.total === 0}\n \n {$t.reports?.empty}\n
\n {:else if collection.items.length === 0 && hasActiveFilters()}\n \n

{$t.reports?.filtered_empty}

\n \n {$t.reports?.clear_filters}\n \n
\n {:else}\n
\n
\n \n
\n \n
\n {/if}\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n null}\n actions={() => null}\n />\n\n
\n
\n \n {#each TASK_TYPE_OPTIONS as option}\n \n {/each}\n \n\n \n {#each STATUS_OPTIONS as option}\n \n {/each}\n \n\n loadReports()}\n >\n {$t.common?.refresh}\n \n\n \n {$t.reports?.clear_filters}\n \n
\n
\n\n {#if loading}\n \n {$t.reports?.loading}\n
\n {:else if error}\n \n

{error}

\n loadReports()}\n >\n {$t.reports?.retry_load || $t.common?.retry}\n \n
\n {:else if !collection || collection.total === 0}\n \n {$t.reports?.empty}\n
\n {:else if collection.items.length === 0 && hasActiveFilters()}\n \n

{$t.reports?.filtered_empty}

\n \n {$t.reports?.clear_filters}\n \n
\n {:else}\n
\n
\n \n
\n \n
\n {/if}\n\n\n\n" }, { "contract_id": "ReportPageContractTest:Module", @@ -100039,6 +105650,426 @@ "tier_source": "AutoCalculated", "body": "// #region llm_report_screenshot_effect_contract:Function [TYPE Function]\n// @RELATION: USES -> App\n// @PURPOSE: Ensure screenshot loading stays untracked from blob-url mutation state.\n// @PRE: Report page source exists.\n// @POST: Contract fails if screenshot loading effect can subscribe to screenshotBlobUrls updates.\ndescribe(\"LLM report screenshot effect contract\", () => {\n it(\"uses untrack around screenshot blob loading side effect\", () => {\n const source = fs.readFileSync(PAGE_PATH, \"utf-8\");\n\n expect(source).toContain('import { onDestroy, onMount, untrack } from \"svelte\";');\n expect(source).toContain(\"untrack(() => {\");\n expect(source).toContain(\"void loadScreenshotBlobUrls(paths);\");\n });\n});\n// #endregion llm_report_screenshot_effect_contract:Function\n" }, + { + "contract_id": "SettingsPage", + "contract_type": "Page", + "file_path": "frontend/src/routes/settings/+page.svelte", + "start_line": 1, + "end_line": 291, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "DEPRECATED": "N/A", + "LAYER": "Page", + "POST": "Page exposes consolidated settings tabs; each tab manages its own state.", + "PRE": "Route is loaded in the authenticated UI shell.", + "PURPOSE": "Consolidated Settings Page shell — thin layout that delegates each tab to its own component.", + "RATIONALE": "Decomposed from 1451→287 lines by extracting 6 tab components and centralizing state in the page shell. The C4 rating reflects the page's role as tab orchestrator with settings load/save lifecycle.", + "REJECTED": "Keeping all tab content inline was rejected due to INV_7 violations (single contract exceeded 150 lines, cyclomatic complexity > 10).", + "REPLACED_BY": "N/A", + "SIDE_EFFECT": "Loads settings data on mount.", + "UX_FEEDBACK": "Toast on save.", + "UX_RECOVERY": "Refresh button reloads settings.", + "UX_STATE": "Error -> Error banner with retry." + }, + "relations": [ + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "EnvironmentsTab", + "target_ref": "[EnvironmentsTab]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "LoggingSettings", + "target_ref": "[LoggingSettings]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "DatabaseConnectionsTab", + "target_ref": "[DatabaseConnectionsTab]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "LlmSettings", + "target_ref": "[LlmSettings]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "MigrationSettings", + "target_ref": "[MigrationSettings]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "StorageSettings", + "target_ref": "[StorageSettings]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "FeaturesSettings", + "target_ref": "[FeaturesSettings]" + }, + { + "source_id": "SettingsPage", + "relation_type": "DEPENDS_ON", + "target_id": "SettingsUtils", + "target_ref": "[SettingsUtils]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "DEPRECATED", + "message": "@DEPRECATED is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "DEPRECATED", + "message": "@DEPRECATED is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "invalid_enum_value", + "tag": "LAYER", + "message": "@LAYER value 'Page' is not in allowed enum", + "detail": { + "allowed": [ + "Core", + "Domain", + "API", + "UI", + "Service", + "Infrastructure", + "Plugin" + ], + "value": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "POST", + "message": "@POST is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PRE", + "message": "@PRE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "RATIONALE", + "message": "@RATIONALE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "REJECTED", + "message": "@REJECTED is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "REPLACED_BY", + "message": "@REPLACED_BY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REPLACED_BY", + "message": "@REPLACED_BY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_FEEDBACK", + "message": "@UX_FEEDBACK is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_RECOVERY", + "message": "@UX_RECOVERY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Component" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n

\n {$t.settings?.title}\n

\n \n {$t.common?.refresh}\n \n
\n\n \n {#if error}\n \n {error}\n \n {$t.common?.retry}\n \n
\n {/if}\n\n \n {#if isLoading}\n
\n
\n
\n
\n
\n
\n
\n
\n
\n {:else if settings}\n \n
\n handleTabChange(\"environments\")}\n >\n {$t.settings?.environments}\n \n handleTabChange(\"logging\")}\n >\n {$t.settings?.logging}\n \n handleTabChange(\"connections\")}\n >\n {$t.settings?.connections}\n \n handleTabChange(\"git\")}\n >\n {$t.nav?.settings_git || $t.nav?.git || \"Git\"}\n \n handleTabChange(\"llm\")}\n >\n {$t.settings?.llm}\n \n handleTabChange(\"migration\")}\n >\n {$t.settings?.migration_sync}\n \n handleTabChange(\"storage\")}\n >\n {$t.settings?.storage}\n \n handleTabChange(\"features\")}\n >\n {$t.settings?.features || \"Features\"}\n \n handleTabChange(\"automation\")}\n >\n {$t.settings?.automation || \"Automation\"}\n \n
\n\n \n
\n {#if activeTab === \"environments\"}\n \n {:else if activeTab === \"logging\"}\n \n {:else if activeTab === \"connections\"}\n \n {:else if activeTab === \"git\"}\n \n {:else if activeTab === \"llm\"}\n \n {:else if activeTab === \"migration\"}\n \n {:else if activeTab === \"storage\"}\n \n {:else if activeTab === \"features\"}\n \n {:else if activeTab === \"automation\"}\n \n {/if}\n
\n {/if}\n\n\n" + }, { "contract_id": "frontend/src/routes/settings/+page.ts::load", "contract_type": "Function", @@ -100054,6 +106085,491 @@ "tier_source": "AutoCalculated", "body": "/** @type {import('./$types').PageLoad} */" }, + { + "contract_id": "DatabaseConnectionsTab", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/DatabaseConnectionsTab.svelte", + "start_line": 1, + "end_line": 342, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "POST": "Connections list is displayed; user can create and delete connections.", + "PRE": "API client is initialized.", + "PURPOSE": "Database connections management tab: list, create, delete external DB connections.", + "UX_FEEDBACK": "Toast on create/delete success/failure.", + "UX_STATE": "FormVisible -> Connection creation form displayed." + }, + "relations": [ + { + "source_id": "DatabaseConnectionsTab", + "relation_type": "CALLS", + "target_id": "api_module", + "target_ref": "[api_module]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

\n {$t.settings?.connections}\n

\n

\n {$t.settings?.connections_description}\n

\n
\n\n {#if connections.length > 0 || isConnectionFormVisible}\n \n {$t.connections?.add_new}\n \n {/if}\n
\n\n {#if isConnectionFormVisible}\n
\n {\n event.preventDefault();\n handleCreateConnection();\n }}\n >\n
\n {$t.connections?.name}\n \n
\n\n
\n
\n Type\n \n \n \n
\n
\n {$t.connections?.host}\n \n
\n
\n\n
\n
\n {$t.connections?.port}\n \n
\n
\n {$t.connections?.db_name}\n \n
\n
\n\n
\n
\n {$t.connections?.user}\n \n
\n
\n {$t.connections?.pass}\n \n
\n
\n\n
\n \n {$t.common?.cancel}\n \n \n {isSubmittingConnection\n ? ($t.settings?.saving || $t.common?.loading || \"Saving...\")\n : $t.connections?.create}\n \n
\n \n
\n {/if}\n\n {#if isLoadingConnections}\n \n {$t.common?.loading || \"Loading...\"}\n
\n {:else if connections.length > 0}\n
\n \n \n \n {$t.connections?.name}\n {$t.connections?.host}\n {$t.connections?.user}\n {$t.settings?.env_actions || \"Actions\"}\n \n \n \n {#each connections as connection}\n \n \n \n \n \n \n {/each}\n \n
\n {connection.name}\n \n {connection.type}://{connection.host}:{connection.port}/{connection.database}\n \n {connection.username}\n \n handleDeleteConnection(connection.id)}\n >\n {$t.connections?.delete}\n \n
\n
\n {:else if !isConnectionFormVisible}\n \n

\n {$t.settings?.no_external_connections}\n

\n {\n openConnectionForm();\n void loadConnections();\n }}\n >\n {$t.connections?.add_new}\n \n \n {:else}\n \n {$t.connections?.no_saved}\n \n {/if}\n\n\n" + }, + { + "contract_id": "FeaturesSettings", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/FeaturesSettings.svelte", + "start_line": 1, + "end_line": 65, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "LAYER": "UI", + "POST": "User can toggle feature flags.", + "PRE": "settings object with features config is provided.", + "PURPOSE": "Feature flags configuration tab: enable/disable top-level application features." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n
\n

\n {$t.settings?.features || \"Features\"}\n

\n

\n {$t.settings?.features_description || \"Enable or disable top-level application features.\"}\n

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

\n {$t.settings?.feature_dataset_review_hint || \"Automatic dataset review, clarification workflow, and SQL execution.\"}\n

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

\n {$t.settings?.feature_health_monitor_hint || \"Dashboard health monitoring and validation report aggregation.\"}\n

\n
\n \n
\n
\n\n
\n \n {$t.settings?.save_features || $t.settings?.save_logging || \"Save\"}\n \n
\n
\n
\n\n" + }, + { + "contract_id": "LlmSettings", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/LlmSettings.svelte", + "start_line": 1, + "end_line": 225, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "POST": "User can configure LLM provider bindings and prompts.", + "PRE": "settings object with llm config and llm_providers list is provided.", + "PURPOSE": "LLM configuration tab: providers, bindings, prompts for chatbot and validation.", + "UX_FEEDBACK": "Toast on save.", + "UX_STATE": "Loaded -> Full LLM settings panel with provider config, bindings, and prompts." + }, + "relations": [ + { + "source_id": "LlmSettings", + "relation_type": "DEPENDS_ON", + "target_id": "ProviderConfig", + "target_ref": "[ProviderConfig]" + }, + { + "source_id": "LlmSettings", + "relation_type": "DEPENDS_ON", + "target_id": "SettingsUtils", + "target_ref": "[SettingsUtils]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n
\n

\n {$t.settings?.llm}\n

\n

\n {$t.settings?.llm_description}\n

\n\n \n\n
\n

\n {$t.settings?.llm_chatbot_settings_title}\n

\n

\n {$t.settings?.llm_chatbot_settings_description}\n

\n\n
\n
\n \n {$t.settings?.llm_chatbot_provider}\n \n \n \n {#each settings.llm_providers || [] as provider}\n \n {/each}\n \n
\n\n
\n \n {$t.settings?.llm_chatbot_model}\n \n \n
\n
\n
\n\n
\n

\n {$t.settings?.llm_provider_bindings_title}\n

\n

\n {$t.settings?.llm_provider_bindings_description}\n

\n\n
\n
\n \n {$t.settings?.llm_binding_dashboard_validation}\n \n \n \n {#each settings.llm_providers || [] as provider}\n \n {/each}\n \n {#if !isDashboardValidationBindingValid(settings)}\n

\n {$t.settings?.llm_multimodal_warning}\n

\n {/if}\n
\n\n
\n \n {$t.settings?.llm_binding_documentation}\n \n \n \n {#each settings.llm_providers || [] as provider}\n \n {/each}\n \n
\n\n
\n \n {$t.settings?.llm_binding_git_commit}\n \n \n \n {#each settings.llm_providers || [] as provider}\n \n {/each}\n \n
\n
\n
\n\n
\n

\n {$t.settings?.llm_prompts_title}\n

\n

\n {$t.settings?.llm_prompts_description}\n

\n\n
\n
\n \n {$t.settings?.llm_prompt_documentation}\n \n \n
\n\n
\n \n {$t.settings?.llm_prompt_dashboard_validation}\n \n \n
\n\n
\n \n {$t.settings?.llm_prompt_git_commit}\n \n \n
\n
\n\n
\n \n {$t.settings?.save_llm_prompts}\n \n
\n
\n
\n\n" + }, + { + "contract_id": "LoggingSettings", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/LoggingSettings.svelte", + "start_line": 1, + "end_line": 88, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "POST": "User can adjust logging levels and toggle belief state.", + "PRE": "settings object with logging config is provided.", + "PURPOSE": "Logging configuration tab: log level, task log level, belief state toggle.", + "UX_FEEDBACK": "Toast on save.", + "UX_STATE": "Loaded -> Form controls for logging configuration." + }, + "relations": [ + { + "source_id": "LoggingSettings", + "relation_type": "DEPENDS_ON", + "target_id": "SettingsUtils", + "target_ref": "[SettingsUtils]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n
\n

\n {$t.settings?.logging}\n

\n

\n {$t.settings?.logging_description}\n

\n\n
\n
\n
\n {$t.settings?.log_level}\n \n \n \n \n \n \n \n
\n
\n {$t.settings?.task_log_level}\n \n \n \n \n \n \n
\n
\n \n

\n {$t.settings?.belief_state_hint}\n

\n
\n
\n\n
\n \n {$t.settings?.save_logging}\n \n
\n
\n
\n\n" + }, + { + "contract_id": "MigrationMappingsTable", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/MigrationMappingsTable.svelte", + "start_line": 1, + "end_line": 278, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "POST": "User can search, filter by environment/type, and paginate through resource mappings.", + "PRE": "API client initialized, refreshKey provided by parent.", + "PURPOSE": "Mappings data table with search, filtering, and pagination for migration sync resources. Isolated from the main MigrationSettings to keep each module < 400 lines.", + "UX_REACTIVITY": "Props -> refreshKey triggers reload via $effect.", + "UX_STATE": "Empty -> \"No mappings\" row." + }, + "relations": [ + { + "source_id": "MigrationMappingsTable", + "relation_type": "DEPENDS_ON", + "target_id": "api_module", + "target_ref": "[api_module]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n
\n

\n \n {$t.settings?.synchronized_resources}\n ({mappingsTotal})\n \n loadMappings()}\n class=\"text-sm text-indigo-600 hover:text-indigo-800 flex items-center gap-1\"\n disabled={isLoading}\n >\n \n {$t.common?.refresh}\n \n

\n\n \n
\n
\n \n
\n \n \n \n \n \n \n \n \n \n \n \n
\n\n
\n \n \n \n {$t.settings?.resource_name}\n {$t.settings?.type}\n UUID\n {$t.settings?.target_id}\n {$t.dashboard?.environment}\n \n \n \n {#if isLoading && displayMappings.length === 0}\n {$t.settings?.loading_mappings}\n {:else if displayMappings.length === 0}\n {mappingsSearch ||\n mappingsEnvFilter ||\n mappingsTypeFilter\n ? $t.settings?.no_matching_resources\n : $t.settings?.no_synchronized_resources}\n {:else}\n {#each displayMappings as mapping}\n \n {mapping.resource_name ||\n $t.common?.not_available}\n \n {/each}\n {/if}\n \n
{mapping.resource_type}\n {mapping.uuid}\n {mapping.remote_id}\n {mapping.environment_id}\n
\n
\n\n \n {#if mappingsTotal > mappingsPageSize}\n
\n {($t.dashboard?.showing)\n .replace(\"{start}\", String(mappingsPage * mappingsPageSize + 1))\n .replace(\n \"{end}\",\n String(\n Math.min(\n (mappingsPage + 1) * mappingsPageSize,\n mappingsTotal,\n ),\n ),\n )\n .replace(\"{total}\", String(mappingsTotal))}\n
\n goToPage(0)}\n disabled={mappingsPage === 0}\n class=\"px-2 py-1 rounded border {mappingsPage === 0\n ? 'bg-gray-100 text-gray-400 cursor-not-allowed'\n : 'bg-white hover:bg-gray-50 text-gray-700'}\">«\n goToPage(mappingsPage - 1)}\n disabled={mappingsPage === 0}\n class=\"px-2 py-1 rounded border {mappingsPage === 0\n ? 'bg-gray-100 text-gray-400 cursor-not-allowed'\n : 'bg-white hover:bg-gray-50 text-gray-700'}\">‹\n {mappingsPage + 1} / {mappingsTotalPages}\n goToPage(mappingsPage + 1)}\n disabled={mappingsPage >= mappingsTotalPages - 1}\n class=\"px-2 py-1 rounded border {mappingsPage >=\n mappingsTotalPages - 1\n ? 'bg-gray-100 text-gray-400 cursor-not-allowed'\n : 'bg-white hover:bg-gray-50 text-gray-700'}\">›\n goToPage(mappingsTotalPages - 1)}\n disabled={mappingsPage >= mappingsTotalPages - 1}\n class=\"px-2 py-1 rounded border {mappingsPage >=\n mappingsTotalPages - 1\n ? 'bg-gray-100 text-gray-400 cursor-not-allowed'\n : 'bg-white hover:bg-gray-50 text-gray-700'}\">»\n
\n
\n {/if}\n
\n\n" + }, + { + "contract_id": "MigrationSettings", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/MigrationSettings.svelte", + "start_line": 1, + "end_line": 139, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "LAYER": "UI", + "POST": "User can configure sync schedule, trigger sync, and browse resource mappings.", + "PRE": "API client initialized.", + "PURPOSE": "Migration sync configuration tab: cron schedule, sync now, mappings table with filtering and pagination.", + "UX_FEEDBACK": "Toast on save/sync/failure.", + "UX_STATE": "Loaded -> Full migration sync panel with schedule and mappings table." + }, + "relations": [ + { + "source_id": "MigrationSettings", + "relation_type": "CALLS", + "target_id": "api_module", + "target_ref": "[api_module]" + } + ], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n
\n

\n {$t.settings?.migration_sync_title}\n

\n

\n {$t.settings?.migration_sync_description}\n

\n\n \n
\n

\n {$t.settings?.sync_schedule}\n

\n
\n
\n {$t.tasks?.cron_label}\n \n

\n {$t.settings?.migration_cron_hint}\n

\n
\n \n {isSavingMigration ? $t.settings?.saving : $t.common?.save}\n \n \n {#if isSyncing}\n \n {$t.settings?.syncing}\n {:else}\n \n {$t.settings?.sync_now}\n {/if}\n \n
\n
\n\n \n
\n\n" + }, + { + "contract_id": "StorageSettings", + "contract_type": "Component", + "file_path": "frontend/src/routes/settings/StorageSettings.svelte", + "start_line": 1, + "end_line": 73, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "LAYER": "UI", + "POST": "User can view and edit storage paths.", + "PRE": "settings object with storage config is provided.", + "PURPOSE": "Storage configuration tab: root path, backup path, repo path." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Component'", + "detail": { + "actual_type": "Component", + "allowed_types": [ + "Module", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Component' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Component" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n
\n

\n {$t.settings?.storage_title}\n

\n

\n {$t.settings?.storage_description}\n

\n\n
\n
\n
\n {$t.storage?.root}\n \n
\n
\n {$t.storage?.backups}\n \n
\n
\n {$t.storage?.repositories}\n \n
\n
\n\n
\n onSave()}\n class=\"bg-primary text-white px-4 py-2 rounded hover:bg-primary-hover\"\n >\n {$t.settings?.save_storage_config}\n \n
\n
\n
\n\n" + }, { "contract_id": "SettingsPageUxTest:Module", "contract_type": "Function", @@ -100329,7 +106845,7 @@ ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n
\n
\n

Automation Policies

\n

Manage scheduled validation rules and execution windows.

\n
\n {#if !showForm}\n \n {/if}\n
\n\n {#if showForm}\n
\n
\n \n |\n

\n {selectedPolicy ? 'Edit Policy' : 'New Policy'}\n

\n
\n showForm = false} \n />\n
\n {:else if isLoading}\n
\n
\n
\n {:else}\n
\n
    \n {#each policies as policy}\n
  • \n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n

    {policy.name}

    \n
    \n {getEnvName(policy.environment_id)}\n \n {policy.dashboard_ids.length} Dashboards\n
    \n
    \n
    \n
    \n \n
    \n \n \n
    \n
    \n
    \n
    \n
  • \n {:else}\n
  • \n \n \n \n

    No policies

    \n

    Get started by creating a new automation policy.

    \n
    \n \n
    \n
  • \n {/each}\n
\n
\n\n {#if translationSchedules.length > 0}\n

Translation Schedules

\n
\n
    \n {#each translationSchedules as ts}\n
  • \n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n

    {ts.job_name}

    \n
    \n {ts.cron_expression}\n \n {ts.timezone}\n
    \n
    \n
    \n
    \n \n {ts.is_active ? 'Active' : 'Inactive'}\n \n {#if ts.last_run_at}\n \n Last: {new Date(ts.last_run_at).toLocaleString()}\n \n {/if}\n
    \n
    \n
    \n
  • \n {:else}\n
  • \n No translation schedules configured\n
  • \n {/each}\n
\n
\n {/if}\n {/if}\n
\n\n" + "body": "\n\n\n\n\n\n
\n
\n
\n

Automation Policies

\n

Manage scheduled validation rules and execution windows.

\n
\n {#if !showForm}\n \n {/if}\n
\n\n {#if showForm}\n
\n
\n \n |\n

\n {selectedPolicy ? 'Edit Policy' : 'New Policy'}\n

\n
\n showForm = false} \n />\n
\n {:else if isLoading}\n
\n
\n
\n {:else}\n
\n
    \n {#each policies as policy}\n
  • \n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n

    {policy.name}

    \n
    \n {getEnvName(policy.environment_id)}\n \n {policy.dashboard_ids.length} Dashboards\n
    \n
    \n
    \n
    \n \n
    \n \n \n
    \n
    \n
    \n
    \n
  • \n {:else}\n
  • \n \n \n \n

    No policies

    \n

    Get started by creating a new automation policy.

    \n
    \n \n
    \n
  • \n {/each}\n
\n
\n\n {#if translationSchedules.length > 0}\n

Translation Schedules

\n
\n
    \n {#each translationSchedules as ts}\n
  • \n
    \n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    \n

    {ts.job_name}

    \n
    \n {ts.cron_expression}\n \n {ts.timezone}\n
    \n
    \n
    \n
    \n \n {ts.is_active ? 'Active' : 'Inactive'}\n \n {#if ts.last_run_at}\n \n Last: {new Date(ts.last_run_at).toLocaleString()}\n \n {/if}\n
    \n
    \n
    \n
  • \n {:else}\n
  • \n No translation schedules configured\n
  • \n {/each}\n
\n
\n {/if}\n {/if}\n
\n\n" }, { "contract_id": "handleSuccess:Function", @@ -100864,7 +107380,7 @@ "contract_type": "Function", "file_path": "frontend/src/routes/settings/settings-utils.js", "start_line": 145, - "end_line": 149, + "end_line": 150, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -100872,14 +107388,14 @@ "schema_warnings": [], "anchor_syntax": "jsdoc", "tier_source": "AutoCalculated", - "body": "/**\n * Strip trailing slashes and the `/api/v1` suffix from a Superset base URL,\n * returning a clean base URL suitable for constructing dashboard links.\n */" + "body": "/**\n * Strip trailing slashes and the `/api/v1` suffix from a Superset base URL,\n * auto-prepend https:// if no scheme is present,\n * returning a clean base URL suitable for constructing dashboard links.\n */" }, { "contract_id": "frontend/src/routes/settings/settings-utils.js::resolveEnvStage", "contract_type": "Function", "file_path": "frontend/src/routes/settings/settings-utils.js", - "start_line": 158, - "end_line": 162, + "start_line": 163, + "end_line": 167, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -100893,8 +107409,8 @@ "contract_id": "frontend/src/routes/settings/settings-utils.js::createEmptyConnectionDraft", "contract_type": "Function", "file_path": "frontend/src/routes/settings/settings-utils.js", - "start_line": 174, - "end_line": 177, + "start_line": 179, + "end_line": 182, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -102036,12 +108552,11 @@ "contract_type": "Page", "file_path": "frontend/src/routes/translate/+page.svelte", "start_line": 1, - "end_line": 282, + "end_line": 279, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Translation job list page showing all jobs with status badges and schedule indicators.", "TYPE": "{string} idle | loading | empty | populated | error" }, "relations": [], @@ -102073,54 +108588,44 @@ } }, { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Page'", + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{string} idle | loading | empty | populated | error' is not in allowed enum", "detail": { - "actual_type": "Page", - "allowed_types": [ + "allowed": [ "Module", "Function", "Class", "Component", "Block", "ADR", + "Tombstone", "Skill", "Agent" - ] + ], + "value": "{string} idle | loading | empty | populated | error" } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Page' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Page" - } - }, - { - "code": "unknown_tag", - "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n */\n\n import { onMount } from 'svelte';\n import { goto } from '$app/navigation';\n import { t, _ } from '$lib/i18n';\n import { addToast } from '$lib/toasts.js';\n import { fetchJobs, deleteJob, duplicateJob } from '$lib/api/translate.js';\n\n /** @type {string} idle | loading | empty | populated | error */\n let uxState = $state('idle');\n let jobs = $state([]);\n let error = $state(null);\n let isLoading = $state(true);\n let showDeleteConfirm = $state(null);\n let statusFilter = $state('');\n let currentPage = $state(1);\n let pageSize = $state(20);\n\n // Count jobs by status for filter pills\n let statusCounts = $derived.by(() => {\n const counts = { DRAFT: 0, READY: 0, RUNNING: 0, COMPLETED: 0, FAILED: 0, CANCELLED: 0 };\n for (const job of jobs) {\n if (counts[job.status] !== undefined) counts[job.status]++;\n }\n return counts;\n });\n\n let statusPills = $derived([\n { label: 'All', value: '', count: jobs.length },\n { label: $t.translate?.jobs?.status_draft, value: 'DRAFT', count: statusCounts.DRAFT },\n { label: $t.translate?.jobs?.status_ready, value: 'READY', count: statusCounts.READY },\n { label: $t.translate?.jobs?.status_running, value: 'RUNNING', count: statusCounts.RUNNING },\n { label: $t.translate?.jobs?.status_completed, value: 'COMPLETED', count: statusCounts.COMPLETED },\n { label: $t.translate?.jobs?.status_failed, value: 'FAILED', count: statusCounts.FAILED },\n ]);\n\n onMount(() => {\n loadJobs();\n });\n\n /** @returns {Promise} */\n async function loadJobs() {\n isLoading = true;\n uxState = 'loading';\n error = null;\n try {\n const result = await fetchJobs({ page: currentPage, page_size: pageSize, status: statusFilter || undefined });\n jobs = Array.isArray(result) ? result : (result?.results || result?.items || []);\n uxState = jobs.length === 0 ? 'empty' : 'populated';\n } catch (err) {\n error = err?.message || $t.translate?.jobs?.load_failed;\n uxState = 'error';\n } finally {\n isLoading = false;\n }\n }\n\n /** @param {string} status */\n function filterByStatus(status) {\n statusFilter = status;\n currentPage = 1;\n loadJobs();\n }\n\n /** @param {string} jobId */\n function navigateToConfig(jobId) {\n goto(`/translate/${jobId}`);\n }\n\n function navigateToCreate() {\n goto('/translate/new');\n }\n\n /** @param {string} jobId */\n async function handleDuplicate(jobId) {\n try {\n const result = await duplicateJob(jobId);\n addToast(`${_('translate.jobs.job_duplicated').replace('{name}', result.name)}`, 'success');\n loadJobs();\n } catch (err) {\n addToast(err?.message || _('translate.jobs.duplicate_failed'), 'error');\n }\n }\n\n /** @param {string} jobId */\n async function handleDelete(jobId) {\n try {\n await deleteJob(jobId);\n addToast(_('translate.jobs.job_deleted'), 'success');\n showDeleteConfirm = null;\n loadJobs();\n } catch (err) {\n addToast(err?.message || _('translate.jobs.delete_failed'), 'error');\n showDeleteConfirm = null;\n }\n }\n\n /** @param {string} status */\n function getStatusBadgeClass(status) {\n const map = {\n DRAFT: 'bg-gray-100 text-gray-700',\n READY: 'bg-blue-100 text-blue-700',\n RUNNING: 'bg-yellow-100 text-yellow-700',\n COMPLETED: 'bg-green-100 text-green-700',\n FAILED: 'bg-red-100 text-red-700',\n CANCELLED: 'bg-gray-100 text-gray-500',\n };\n return map[status] || 'bg-gray-100 text-gray-700';\n }\n\n\n
\n
\n
\n

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

\n

\n {$t.translate?.jobs?.subtitle}\n

\n
\n \n \n \n \n {$t.translate?.jobs?.new_job}\n \n
\n\n \n
\n {#each statusPills as pill}\n filterByStatus(pill.value)}\n class=\"px-3 py-1.5 text-sm rounded-full border transition-colors\n {statusFilter === pill.value\n ? 'bg-blue-50 border-blue-300 text-blue-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n >\n {pill.label}\n ({pill.count})\n \n {/each}\n
\n\n \n {#if isLoading}\n
\n {#each Array(5) as _}\n
\n {/each}\n
\n\n \n {:else if uxState === 'error'}\n
\n

{error || $t.translate?.jobs?.an_error_occurred}

\n \n {$t.translate?.common?.retry}\n \n
\n\n \n {:else if uxState === 'empty'}\n
\n \n \n \n {#if statusFilter}\n

{$t.translate?.jobs?.no_jobs_filtered.replace('{status}', statusFilter)}

\n filterByStatus('')}\n class=\"text-blue-600 hover:text-blue-700 text-sm\"\n >\n {$t.translate?.jobs?.clear_filter}\n \n {:else}\n

{$t.translate?.jobs?.no_jobs}

\n \n {$t.translate?.jobs?.create_job}\n \n {/if}\n
\n\n \n {:else if uxState === 'populated'}\n
\n {#each jobs as job}\n navigateToConfig(job.id)}\n class=\"bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-gray-300 transition-all cursor-pointer\"\n >\n
\n
\n
\n

{job.name}

\n \n {job.status}\n \n
\n {#if job.description}\n

{job.description}

\n {/if}\n
\n {#if job.source_dialect && job.target_dialect}\n {job.source_dialect} → {job.target_dialect}\n {/if}\n\t\t\t\t{#if job.target_languages?.length > 0}\n\t\t\t\t
\n\t\t\t\t {#each job.target_languages as lang}\n\t\t\t\t {lang}\n\t\t\t\t {/each}\n\t\t\t\t
\n\t\t\t\t{:else if job.target_language}\n\t\t\t\t {job.target_language}\n\t\t\t\t{/if}\n {#if job.translation_column}\n {$t.translate?.jobs?.column_label.replace('{column}', job.translation_column)}\n {/if}\n {#if job.created_by}\n {$t.translate?.jobs?.by_label.replace('{by}', job.created_by)}\n {/if}\n {#if job.updated_at}\n {$t.translate?.jobs?.updated_label.replace('{date}', new Date(job.updated_at).toLocaleDateString())}\n {/if}\n
\n
\n
e.stopPropagation()}>\n handleDuplicate(job.id)}\n class=\"p-2 text-gray-400 hover:text-blue-600 rounded-lg hover:bg-blue-50 transition-colors\"\n title={$t.translate?.jobs?.duplicate}\n >\n \n \n \n \n {#if showDeleteConfirm === job.id}\n
\n handleDelete(job.id)}\n class=\"px-2 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700\"\n >\n {$t.translate?.jobs?.confirm_delete}\n \n (showDeleteConfirm = null)}\n class=\"px-2 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300\"\n >\n {$t.translate?.jobs?.cancel_delete}\n \n
\n {:else}\n (showDeleteConfirm = job.id)}\n class=\"p-2 text-gray-400 hover:text-red-600 rounded-lg hover:bg-red-50 transition-colors\"\n title={$t.translate?.common?.delete}\n >\n \n \n \n \n {/if}\n
\n
\n
\n {/each}\n
\n {/if}\n
\n\n\n" + "body": "\n\n\n
\n
\n
\n

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

\n

\n {$t.translate?.jobs?.subtitle}\n

\n
\n \n \n \n \n {$t.translate?.jobs?.new_job}\n \n
\n\n \n
\n {#each statusPills as pill}\n filterByStatus(pill.value)}\n class=\"px-3 py-1.5 text-sm rounded-full border transition-colors\n {statusFilter === pill.value\n ? 'bg-primary-light border-blue-300 text-blue-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n >\n {pill.label}\n ({pill.count})\n \n {/each}\n
\n\n \n {#if isLoading}\n
\n {#each Array(5) as _}\n
\n {/each}\n
\n\n \n {:else if uxState === 'error'}\n
\n

{error || $t.translate?.jobs?.an_error_occurred}

\n \n {$t.translate?.common?.retry}\n \n
\n\n \n {:else if uxState === 'empty'}\n
\n \n \n \n {#if statusFilter}\n

{$t.translate?.jobs?.no_jobs_filtered.replace('{status}', statusFilter)}

\n filterByStatus('')}\n class=\"text-primary hover:text-primary-hover text-sm\"\n >\n {$t.translate?.jobs?.clear_filter}\n \n {:else}\n

{$t.translate?.jobs?.no_jobs}

\n \n {$t.translate?.jobs?.create_job}\n \n {/if}\n
\n\n \n {:else if uxState === 'populated'}\n
\n {#each jobs as job}\n navigateToConfig(job.id)}\n class=\"bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-gray-300 transition-all cursor-pointer\"\n >\n
\n
\n
\n

{job.name}

\n \n {job.status}\n \n
\n {#if job.description}\n

{job.description}

\n {/if}\n
\n {#if job.source_dialect && job.target_dialect}\n {job.source_dialect} → {job.target_dialect}\n {/if}\n\t\t\t\t{#if job.target_languages?.length > 0}\n\t\t\t\t
\n\t\t\t\t {#each job.target_languages as lang}\n\t\t\t\t {lang}\n\t\t\t\t {/each}\n\t\t\t\t
\n\t\t\t\t{:else if job.target_language}\n\t\t\t\t {job.target_language}\n\t\t\t\t{/if}\n {#if job.translation_column}\n {$t.translate?.jobs?.column_label.replace('{column}', job.translation_column)}\n {/if}\n {#if job.created_by}\n {$t.translate?.jobs?.by_label.replace('{by}', job.created_by)}\n {/if}\n {#if job.updated_at}\n {$t.translate?.jobs?.updated_label.replace('{date}', new Date(job.updated_at).toLocaleDateString())}\n {/if}\n
\n
\n
e.stopPropagation()}>\n handleDuplicate(job.id)}\n class=\"p-2 text-gray-400 hover:text-blue-600 rounded-lg hover:bg-blue-50 transition-colors\"\n title={$t.translate?.jobs?.duplicate}\n >\n \n \n \n \n {#if showDeleteConfirm === job.id}\n
\n handleDelete(job.id)}\n class=\"px-2 py-1 text-xs bg-destructive text-white rounded hover:bg-destructive-hover\"\n >\n {$t.translate?.jobs?.confirm_delete}\n \n (showDeleteConfirm = null)}\n class=\"px-2 py-1 text-xs bg-gray-200 text-gray-700 rounded hover:bg-gray-300\"\n >\n {$t.translate?.jobs?.cancel_delete}\n \n
\n {:else}\n (showDeleteConfirm = job.id)}\n class=\"p-2 text-gray-400 hover:text-red-600 rounded-lg hover:bg-red-50 transition-colors\"\n title={$t.translate?.common?.delete}\n >\n \n \n \n \n {/if}\n
\n
\n
\n {/each}\n
\n {/if}\n
\n\n\n" }, { "contract_id": "TranslationJobConfig", "contract_type": "Page", "file_path": "frontend/src/routes/translate/[id]/+page.svelte", "start_line": 1, - "end_line": 1381, + "end_line": 1461, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, - "PURPOSE": "Translation job configuration page with datasource selector, column mapping, target table config, LLM provider, and dictionary attachment.", + "DEPRECATED": "N/A — active page.", + "PURPOSE": "Translation job configuration page - orchestrates form state, datasource loading, LLM settings, run/schedule tabs.", + "RATIONALE": "Kept as C4 orchestration due to complex lifecycle: load→configure→validate→save→run→monitor across multiple tabs and API interactions.", + "REJECTED": "Extracting all sub-tabs into separate page components was rejected due to deep state coupling between datasource selection, column loading, and form validation — would require a global store or excessive prop drilling.", + "REPLACED_BY": "N/A — no replacement.", "TYPE": "{string} idle | loading | configured | saving | validation_error | datasource_unavailable" }, "relations": [], @@ -102151,6 +108656,33 @@ "contract_type": "Page" } }, + { + "code": "tag_not_for_contract_type", + "tag": "DEPRECATED", + "message": "@DEPRECATED is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "DEPRECATED", + "message": "@DEPRECATED is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -102179,27 +108711,208 @@ } }, { - "code": "unknown_tag", + "code": "tag_not_for_contract_type", + "tag": "RATIONALE", + "message": "@RATIONALE is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "REJECTED", + "message": "@REJECTED is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "REPLACED_BY", + "message": "@REPLACED_BY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REPLACED_BY", + "message": "@REPLACED_BY is forbidden for contract type 'Page' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Page" + } + }, + { + "code": "invalid_enum_value", "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TYPE value '{string} idle | loading | configured | saving | validation_error | datasource_unavailable' is not in allowed enum", + "detail": { + "allowed": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "ADR", + "Tombstone", + "Skill", + "Agent" + ], + "value": "{string} idle | loading | configured | saving | validation_error | datasource_unavailable" + } } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n * LLM provider selection, target language, prompt settings, and dictionary attachment.\n * @LAYER UI\n * @RELATION DEPENDS_ON -> [TranslateApi]\n * @RELATION DEPENDS_ON -> [api_module]\n * @PRE Route param id may be \"new\" for create mode or a valid job UUID for edit mode.\n * @POST Job is created or updated with all configuration persisted.\n *\n * @UX_STATE idle -> Initial state before loading\n * @UX_STATE loading -> Fetching job data (edit mode) or datasource metadata\n * @UX_STATE configured -> Form is ready with loaded data\n * @UX_STATE saving -> Save operation in progress\n * @UX_STATE validation_error -> Form validation failure\n * @UX_STATE datasource_unavailable -> Selected datasource cannot be reached\n *\n * @UX_REACTIVITY columnList is $derived from selected datasource\n */\n\n import { onMount, onDestroy } from 'svelte';\n import { goto } from '$app/navigation';\n import { page } from '$app/state';\n import { t, _ } from '$lib/i18n';\n import { addToast } from '$lib/toasts.js';\n import { api } from '$lib/api.js';\n import {\n fetchJobs,\n createJob,\n updateJob,\n fetchDatasourceColumns,\n fetchDatasources,\n } from '$lib/api/translate.js';\n import MultiSelect from '$lib/components/ui/MultiSelect.svelte';\n import TranslationPreview from '$lib/components/translate/TranslationPreview.svelte';\n import TranslationRunProgress from '$lib/components/translate/TranslationRunProgress.svelte';\n import TranslationRunResult from '$lib/components/translate/TranslationRunResult.svelte';\n import ScheduleConfig from '$lib/components/translate/ScheduleConfig.svelte';\n\timport { triggerRun, fetchRunHistory, cancelRun } from '$lib/api/translate.js';\n\timport BulkReplaceModal from '$lib/components/translate/BulkReplaceModal.svelte';\n\timport { fromStore } from 'svelte/store';\n\timport {\n\t\ttranslationRunStore,\n\t\tstartTranslationRun,\n\t\tresetTranslationRun,\n\t\tclearOnCompleteCallback,\n\t} from '$lib/stores/translationRun.js';\n\n const LANGUAGE_LABELS = {\n ru: 'Русский',\n en: 'English',\n de: 'Deutsch',\n fr: 'Français',\n es: 'Español',\n it: 'Italiano',\n pt: 'Português',\n zh: '中文',\n ja: '日本語',\n ko: '한국어',\n ar: 'العربية',\n tr: 'Türkçe',\n nl: 'Nederlands',\n pl: 'Polski',\n sv: 'Svenska',\n da: 'Dansk',\n fi: 'Suomi',\n cs: 'Čeština',\n hu: 'Magyar',\n ro: 'Română',\n vi: 'Tiếng Việt',\n th: 'ไทย',\n he: 'עברית',\n id: 'Bahasa Indonesia',\n ms: 'Bahasa Melayu',\n };\n\n const LANGUAGES = Object.entries(LANGUAGE_LABELS).map(([code, name]) => ({ code, name }));\n\n /** @type {string} idle | loading | configured | saving | validation_error | datasource_unavailable */\n let uxState = $state('idle');\n let isNewJob = $derived(page.params.id === 'new');\n let jobId = $derived(page.params.id);\n let existingJob = $state(null);\n let error = $state(null);\n\n // Form state\n let name = $state('');\n let description = $state('');\n let sourceDialect = $state('postgresql');\n let targetDialect = $state('postgresql');\n let sourceDatasourceId = $state('');\n let sourceTable = $state('');\n let targetSchema = $state('');\n let targetTable = $state('');\n let sourceKeyCols = $state([]);\n let targetKeyCols = $state([]);\n let translationColumn = $state('');\n let targetColumn = $state('');\n let targetLanguageColumn = $state('');\n let targetSourceColumn = $state('');\n let targetSourceLanguageColumn = $state('');\n let contextColumns = $state([]);\n let targetLanguages = $state(['en']);\n let includeSourceReference = $state(true);\n let providerId = $state('');\n let batchSize = $state(50);\n let disableReasoning = $state(false);\n let upsertStrategy = $state('MERGE');\n let dictionaryIds = $state([]);\n let status = $state('DRAFT');\n\n // Datasource & env state\n let environmentId = $state('');\n let environments = $state([]);\n let datasourceId = $state('');\n let datasourceSearch = $state('');\n let datasourceList = $state([]);\n let datasourceLoading = $state(false);\n let showDatasourceDropdown = $state(false);\n let availableColumns = $state([]);\n let virtualColumns = $state([]);\n let databaseDialect = $state('');\n let isColumnsLoading = $state(false);\n let llmProviders = $state([]);\n let availableDictionaries = $state([]);\n let targetDatabaseId = $state('');\n let databases = $state([]);\n let databasesLoading = $state(false);\n\n // Tab state\n /** @type {'config'|'preview'|'run'|'schedule'} */\n let activeTab = $state('config');\n let tabs = $derived.by(() => {\n const base = [\n { id: 'config', label: $t.translate?.config?.basic_info || 'Configuration' },\n { id: 'preview', label: $t.translate?.preview?.title || 'Preview' },\n { id: 'run', label: $t.translate?.config?.run_translation || 'Run' },\n ];\n if (!isNewJob && existingJob) {\n base.push({ id: 'schedule', label: $t.translate?.schedule?.tab_label || 'Schedule' });\n }\n return base;\n });\n\n // Derived\n let columnList = $derived(availableColumns);\n let logicalColumns = $derived(availableColumns.filter(c => c.is_physical !== false));\n let virtualColumnNames = $derived(virtualColumns);\n\n let isSaving = $state(false);\n let validationErrors = $state({});\n let warnings = $state([]);\n let pageTitle = $derived.by(() => {\n if (isNewJob) return $t.translate?.config?.new_title || 'New Translation Job';\n const jobName = existingJob?.name || name;\n const base = $t.translate?.config?.edit_title || 'Edit Translation Job';\n return jobName ? `${jobName} | ${base}` : base;\n });\n\n // Run state — using global store to survive page navigation\n let runState = fromStore(translationRunStore);\n let currentRunId = $derived(runState.current?.runId || null);\n let completedRuns = $state([]);\n let isRunning = $state(false);\n let isFullRun = $state(false);\n\tlet runError = $state('');\n\tlet showPageBulkReplace = $state(false);\n\n async function handleTriggerRun(full = false) {\n isRunning = true;\n isFullRun = full;\n runError = '';\n try {\n const run = await triggerRun(jobId, full);\n startTranslationRun(run.id, { jobId, isFullRun: full, onComplete: handleRunComplete });\n addToast(full ? 'Полный перевод запущен (все строки)' : _('translate.config.run_started'), 'success');\n } catch (err) {\n runError = err?.message || _('translate.config.run_failed');\n isRunning = false;\n }\n }\n\n function handleRunComplete(statusData) {\n if (!isRunning) return; // Idempotent: already handled\n isRunning = false;\n // Don't resetTranslationRun() here — the store's own polling detects the\n // terminal state and stops automatically, keeping the terminal UX visible\n // in the global indicator (TranslationRunGlobalIndicator) until auto-dismiss.\n loadRunHistory();\n const statusLabel = statusData?.status || _('translate.run.completed');\n addToast(`${_('translate.run.run_id')} ${statusLabel.toLowerCase()}`, 'info');\n }\n\n async function loadRunHistory() {\n try {\n const data = await fetchRunHistory(jobId, { page_size: 5 });\n completedRuns = data?.items || [];\n } catch (_err) {\n completedRuns = [];\n }\n }\n\n async function handleRetryRun() {\n if (currentRunId) {\n try {\n await cancelRun(currentRunId);\n } catch (_e) { /* ignore */ }\n }\n resetTranslationRun();\n await handleTriggerRun();\n }\n\n async function handleRetryInsert() {\n if (currentRunId) {\n try {\n const result = await api.postApi(`/translate/runs/${currentRunId}/retry-insert`, {});\n addToast(_('translate.config.insert_retry_started'), 'success');\n } catch (err) {\n addToast(err?.message || _('translate.config.insert_retry_failed'), 'error');\n }\n }\n }\n\n onMount(async () => {\n // Load environments first\n try {\n const envs = await api.getEnvironments?.() || [];\n environments = Array.isArray(envs) ? envs : [];\n if (environments.length > 0 && !environmentId) {\n environmentId = environments[0]?.id || '';\n }\n } catch (_e) {}\n\n await loadInitialData();\n if (!isNewJob) {\n await loadJob();\n } else {\n if (environmentId) {\n await loadDatabases();\n }\n uxState = 'configured';\n }\n });\n\n // Clean up store callback when page unmounts — the store keeps\n // polling so the global indicator in the layout still works.\n onDestroy(() => {\n clearOnCompleteCallback();\n });\n\n /** @returns {Promise} */\n async function loadInitialData() {\n try {\n // Load LLM providers\n try {\n const consolidated = await api.getConsolidatedSettings?.() || {};\n llmProviders = consolidated.llm_providers || [];\n // Also try settings endpoint\n if (llmProviders.length === 0 && api.getLlmStatus) {\n const llmStatus = await api.getLlmStatus().catch(() => ({}));\n if (llmStatus?.providers) {\n llmProviders = llmStatus.providers;\n }\n }\n } catch (_e) {\n llmProviders = [];\n }\n\n // Load dictionaries (from available translate dictionaries)\n try {\n const result = await api.fetchApi('/translate/dictionaries').catch(() => ({}));\n availableDictionaries = Array.isArray(result) ? result : (result?.items || []);\n } catch (_e) {\n availableDictionaries = [];\n }\n } catch (err) {\n console.warn('[translate/config] Failed to load some initial data:', err);\n }\n }\n\n /** @returns {Promise} */\n async function loadJob() {\n uxState = 'loading';\n try {\n const jobs = await fetchJobs({ page_size: 100 });\n const jobsArr = Array.isArray(jobs) ? jobs : (jobs?.results || []);\n const job = jobsArr.find(j => j.id === jobId);\n if (!job) {\n // Try direct fetch\n try {\n const directJob = await api.fetchApi(`/translate/jobs/${jobId}`);\n existingJob = directJob;\n } catch (_e) {\n error = _('translate.config.job_not_found').replace('{id}', jobId);\n uxState = 'error';\n return;\n }\n } else {\n existingJob = job;\n }\n\n // Populate form\n const j = existingJob;\n name = j.name || '';\n description = j.description || '';\n sourceDialect = j.source_dialect || 'postgresql';\n targetDialect = j.target_dialect || 'postgresql';\n sourceDatasourceId = j.source_datasource_id || '';\n sourceTable = j.source_table || '';\n targetSchema = j.target_schema || '';\n targetTable = j.target_table || '';\n sourceKeyCols = j.source_key_cols || [];\n targetKeyCols = j.target_key_cols || [];\n translationColumn = j.translation_column || '';\n targetColumn = j.target_column || '';\n targetLanguageColumn = j.target_language_column || '';\n targetSourceColumn = j.target_source_column || '';\n targetSourceLanguageColumn = j.target_source_language_column || '';\n contextColumns = j.context_columns || [];\n targetLanguages = j.target_languages?.length > 0 ? j.target_languages : (j.target_language ? [j.target_language] : ['en']);\n providerId = j.provider_id || '';\n batchSize = j.batch_size || 50;\n disableReasoning = j.disable_reasoning || false;\n upsertStrategy = j.upsert_strategy || 'MERGE';\n dictionaryIds = j.dictionary_ids || [];\n status = j.status || 'DRAFT';\n\n // Restore target database\n if (j.target_database_id) {\n targetDatabaseId = j.target_database_id;\n }\n\n // Load databases for the environment\n if (j.environment_id) {\n await loadDatabases();\n }\n\n // If there's a datasource, load its columns\n if (j.source_datasource_id && environments.length > 0) {\n datasourceId = j.source_datasource_id;\n // Restore the search display from saved table info\n if (j.source_table) {\n const dialect = j.database_dialect || '';\n datasourceSearch = dialect\n ? `${j.source_table} (${dialect})`\n : j.source_table;\n } else {\n datasourceSearch = `Dataset #${j.source_datasource_id}`;\n }\n // Restore environment from stored job, fall back to first available\n if (j.environment_id && environments.some(e => e.id === j.environment_id)) {\n environmentId = j.environment_id;\n } else {\n environmentId = environments[0]?.id || '';\n }\n await loadColumnsForDatasource();\n }\n\n uxState = 'configured';\n // Load run history\n await loadRunHistory();\n } catch (err) {\n error = err?.message || 'Failed to load job';\n uxState = 'error';\n }\n }\n\n /** @returns {Promise} */\n async function loadColumnsForDatasource() {\n if (!datasourceId || !environmentId) {\n availableColumns = [];\n virtualColumns = [];\n return;\n }\n\n isColumnsLoading = true;\n try {\n const result = await fetchDatasourceColumns(parseInt(datasourceId), environmentId);\n if (result) {\n availableColumns = result.columns || [];\n databaseDialect = result.database_dialect || '';\n virtualColumns = availableColumns.filter(c => c.is_physical === false).map(c => c.name);\n }\n } catch (err) {\n uxState = 'datasource_unavailable';\n addToast(`${_('common.error')}: ${err?.message}`, 'error');\n } finally {\n isColumnsLoading = false;\n }\n }\n\n /** Load all datasets (or search) */\n async function searchDatasources(searchTerm = '') {\n if (!environmentId) return;\n datasourceLoading = true;\n try {\n datasourceList = await fetchDatasources(environmentId, searchTerm);\n showDatasourceDropdown = true;\n } catch (err) {\n datasourceList = [];\n } finally {\n datasourceLoading = false;\n }\n }\n\n async function handleDatasourceSearch() {\n await searchDatasources(datasourceSearch);\n }\n\n /** Select a dataset from the dropdown */\n function selectDatasource(ds) {\n datasourceId = String(ds.id);\n datasourceSearch = `${ds.table_name} (${ds.database_name} · ${ds.database_dialect})`;\n showDatasourceDropdown = false;\n availableColumns = [];\n virtualColumns = [];\n sourceKeyCols = [];\n targetKeyCols = [];\n translationColumn = '';\n targetColumn = '';\n targetLanguageColumn = '';\n targetSourceColumn = '';\n targetSourceLanguageColumn = '';\n contextColumns = [];\n databaseDialect = ds.database_dialect || '';\n if (datasourceId && environmentId) {\n loadColumnsForDatasource();\n }\n }\n\n /** @param {Event} event */\n async function handleDatasourceChange(event) {\n const val = event.target.value;\n datasourceId = val;\n availableColumns = [];\n virtualColumns = [];\n // Reset column selections when datasource changes\n sourceKeyCols = [];\n targetKeyCols = [];\n translationColumn = '';\n contextColumns = [];\n\n if (val && environmentId) {\n await loadColumnsForDatasource();\n }\n }\n\n async function loadDatabases() {\n if (!environmentId) {\n databases = [];\n return;\n }\n databasesLoading = true;\n try {\n databases = await api.getEnvironmentDatabases(environmentId) || [];\n } catch (_e) {\n databases = [];\n } finally {\n databasesLoading = false;\n }\n }\n\n /** @param {Event} event */\n async function handleEnvChange(event) {\n const newEnv = event.target.value;\n environmentId = newEnv;\n // Clear search state, but KEEP datasourceId if already loaded from job\n // — just reload columns with the new environment\n datasourceSearch = '';\n datasourceList = [];\n databaseDialect = '';\n targetDatabaseId = '';\n await loadDatabases();\n if (datasourceId) {\n await loadColumnsForDatasource();\n }\n }\n\n /** @param {string} col */\n function toggleContextColumn(col) {\n if (contextColumns.includes(col)) {\n contextColumns = contextColumns.filter(c => c !== col);\n } else {\n contextColumns = [...contextColumns, col];\n }\n }\n\n /** @param {string} col */\n function toggleSourceKeyCol(col) {\n const idx = sourceKeyCols.indexOf(col);\n if (idx >= 0) {\n sourceKeyCols = sourceKeyCols.filter((_, i) => i !== idx);\n // Also remove corresponding target key\n if (targetKeyCols[idx]) {\n targetKeyCols = targetKeyCols.filter((_, i) => i !== idx);\n }\n } else {\n sourceKeyCols = [...sourceKeyCols, col];\n targetKeyCols = [...targetKeyCols, ''];\n }\n }\n\n /** @param {number} idx @param {string} val */\n function updateTargetKeyCol(idx, val) {\n const updated = [...targetKeyCols];\n updated[idx] = val;\n targetKeyCols = updated;\n }\n\n /** @returns {string[]} */\n function validate() {\n const errs = {};\n const warns = [];\n\n if (!name.trim()) {\n errs.name = _('translate.config.name_required');\n }\n\n if (targetLanguages.length === 0) {\n errs.targetLanguages = 'At least one target language is required';\n }\n\n if (!translationColumn) {\n errs.translationColumn = _('translate.config.translation_column_required');\n }\n\n if (sourceKeyCols.length > 0 && sourceKeyCols.some((_, i) => !targetKeyCols[i])) {\n errs.targetKeyCols = _('translate.config.key_mapping_required');\n }\n\n // Warn about virtual key columns\n for (const col of sourceKeyCols) {\n if (virtualColumnNames.includes(col)) {\n warns.push(_('translate.config.virtual_column_warning').replace('{col}', col));\n }\n }\n\n // Check duplicate column mapping\n if (contextColumns.includes(translationColumn)) {\n warns.push(_('translate.config.redundant_column_warning'));\n }\n\n validationErrors = errs;\n warnings = warns;\n return Object.keys(errs);\n }\n\n /** @returns {Promise} */\n async function handleSave() {\n const errs = validate();\n if (errs.length > 0) {\n uxState = 'validation_error';\n return;\n }\n\n isSaving = true;\n uxState = 'saving';\n const payload = {\n name: name.trim(),\n description: description.trim() || null,\n source_dialect: sourceDialect,\n target_dialect: targetDialect,\n source_datasource_id: datasourceId || null,\n source_table: sourceTable || null,\n target_schema: targetSchema || null,\n target_table: targetTable || null,\n source_key_cols: sourceKeyCols,\n target_key_cols: targetKeyCols,\n translation_column: translationColumn || null,\n target_column: targetColumn || null,\n target_language_column: targetLanguageColumn || null,\n target_source_column: targetSourceColumn || null,\n target_source_language_column: targetSourceLanguageColumn || null,\n context_columns: contextColumns,\n target_languages: targetLanguages?.length > 0 ? targetLanguages : null,\n provider_id: providerId || null,\n batch_size: parseInt(batchSize) || 50,\n disable_reasoning: disableReasoning,\n upsert_strategy: upsertStrategy,\n dictionary_ids: dictionaryIds,\n database_dialect: databaseDialect || sourceDialect,\n environment_id: environmentId || null,\n target_database_id: targetDatabaseId || null,\n status: status,\n };\n\n try {\n if (isNewJob) {\n const created = await createJob(payload);\n addToast(_('translate.config.job_created'), 'success');\n goto(`/translate/${created.id}`);\n } else {\n await updateJob(jobId, payload);\n addToast(_('translate.config.job_updated'), 'success');\n uxState = 'configured';\n }\n } catch (err) {\n addToast(err?.message || _('translate.config.save_failed'), 'error');\n uxState = 'validation_error';\n } finally {\n isSaving = false;\n }\n }\n\n /** @param {string} col */\n function isVirtual(col) {\n return virtualColumnNames.includes(col);\n }\n\n function getJobStatusLabel(value) {\n const labels = {\n DRAFT: $t.translate?.config?.status_draft || 'Draft',\n READY: $t.translate?.config?.status_ready || 'Ready',\n ACTIVE: $t.translate?.config?.status_active || 'Active',\n RUNNING: $t.translate?.jobs?.status_running || 'Running',\n COMPLETED: $t.translate?.jobs?.status_completed || 'Completed',\n FAILED: $t.translate?.jobs?.status_failed || 'Failed',\n CANCELLED: $t.translate?.history?.status_cancelled || 'Cancelled',\n };\n return labels[value] || value || ($t.translate?.config?.status_draft || 'Draft');\n }\n\n\n\n {pageTitle}\n\n\n
\n \n
\n
\n

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

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

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

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

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

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

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

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

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

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

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

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

{validationErrors.name}

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

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

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

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

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

{validationErrors.translationColumn}

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

{validationErrors.targetKeyCols}

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

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

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

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

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

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

\n {:else if !environmentId}\n

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

\n {:else}\n

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

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

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

\n

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

\n
\n
\n \n \n

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

\n
\n
\n \n \n

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

\n
\n
\n \n \n

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

\n
\n
\n \n \n

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

\n

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

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

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

\n

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

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

{runError}

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

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

\n showPageBulkReplace = true}\n class=\"px-3 py-1.5 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors\"\n >\n {$t.translate?.run?.bulk_replace || 'Bulk Replace'}\n \n
\n
\n {#each completedRuns as run}\n \n {/each}\n
\n
\n {/if}\n
\n
\n {:else}\n
\n

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

\n
\n {/if}\n
\n {/if}\n
\n\n showPageBulkReplace = false}\n\tonApplied={(count) => { showPageBulkReplace = false; loadRunHistory(); }}\n/>\n\n" + "body": "\n\n\n\n\n\n\n\n\n {pageTitle}\n\n\n
\n \n
\n
\n

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

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

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

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

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

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

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

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

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

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

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

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

{validationErrors.name}

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

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

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

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

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

{validationErrors.translationColumn}

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

{validationErrors.targetKeyCols}

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

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

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

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

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

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

\n {:else if !environmentId}\n

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

\n {:else}\n

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

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

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

\n

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

\n
\n
\n \n \n

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

\n
\n
\n \n \n

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

\n
\n
\n \n \n

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

\n
\n
\n \n \n

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

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

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

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

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

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

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

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

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

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

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

\n

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

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

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

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

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

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

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

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

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

\n

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

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

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

\n

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

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

{runError}

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

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

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

{run.error_message}

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

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

\n
\n {/if}\n
\n {/if}\n
\n\n showPageBulkReplace = false}\n\tonApplied={(count) => { showPageBulkReplace = false; loadRunHistory(); }}\n/>\n\n" + }, + { + "contract_id": "DictionariesPage", + "contract_type": "Page", + "file_path": "frontend/src/routes/translate/dictionaries/+page.svelte", + "start_line": 1, + "end_line": 270, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n
\n {#snippet pageActions()}\n {#if !showCreateForm}\n \n {/if}\n {/snippet}\n\n \n\n {#if showCreateForm}\n \n
\n
\n \n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n
\n
\n \n {\n showCreateForm = false;\n createForm = { name: '', source_dialect: '', target_dialect: '', description: '' };\n }}\n >\n {$t.translate?.common?.cancel}\n \n
\n
\n
\n {/if}\n\n {#if state === 'loading'}\n \n
\n {#each [1, 2, 3] as _}\n
\n {/each}\n
\n
\n {:else if state === 'empty'}\n \n
\n

{$t.translate?.dictionaries?.no_dicts}

\n \n
\n
\n {:else if state === 'populated'}\n \n
\n {#each dictionaries as dict}\n
\n
\n goToEditor(dict.id)}\n >\n

{dict.name}

\n

\n {dict.source_dialect} → {dict.target_dialect}\n {#if dict.description}\n — {dict.description}\n {/if}\n

\n

\n {$t.translate?.dictionaries?.entry_count.replace('{count}', dict.entry_count ?? 0)}\n {#if !dict.is_active}\n {$t.translate?.dictionaries?.inactive}\n {/if}\n

\n \n
\n
\n goToEditor(dict.id)}\n >\n {$t.translate?.common?.edit}\n \n {#if deleteConfirmId === dict.id}\n
\n {$t.translate?.dictionaries?.confirm_delete}\n handleDelete(dict.id)}\n >\n {$t.translate?.common?.delete}\n \n \n {$t.translate?.common?.cancel}\n \n
\n {:else}\n openDeleteConfirm(dict.id)}\n >\n {$t.translate?.common?.delete}\n \n {/if}\n
\n
\n {/each}\n
\n
\n\n {#if total > page_size}\n
\n { currentPage--; loadDictionaries(); }}\n >\n {$t.translate?.common?.previous}\n \n \n {$t.translate?.dictionaries?.page_info.replace('{current}', currentPage).replace('{total}', Math.ceil(total / page_size))}\n \n = Math.ceil(total / page_size)}\n onclick={() => { currentPage++; loadDictionaries(); }}\n >\n {$t.translate?.common?.next}\n \n
\n {/if}\n {/if}\n
\n\n\n" + }, + { + "contract_id": "DictionaryDetailPage", + "contract_type": "Page", + "file_path": "frontend/src/routes/translate/dictionaries/[id]/+page.svelte", + "start_line": 1, + "end_line": 821, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_not_for_contract_type", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is not allowed for contract type 'Page'", + "detail": { + "actual_type": "Page", + "allowed_types": [ + "Module", + "Function", + "Class", + "Component", + "Block", + "Skill", + "Agent" + ] + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "COMPLEXITY", + "message": "@COMPLEXITY is forbidden for contract type 'Page' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Page" + } + } + ], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n
\n {#snippet subtitleContent()}\n {#if dictionary}\n

\n {dictionary.source_dialect} → {dictionary.target_dialect}\n {#if dictionary.description}\n — {dictionary.description}\n {/if}\n · {$t.translate?.dictionaries?.entry_count.replace('{count}', totalEntries)}\n

\n {/if}\n {/snippet}\n\n {#snippet dictActions()}\n {#if state === 'editing'}\n
\n \n \n \n
\n {/if}\n {/snippet}\n\n \n\n {#if state === 'loading'}\n \n
\n {#each [1, 2, 3] as _}\n
\n {/each}\n
\n
\n\n {:else if state === 'importing' || state === 'import_preview'}\n \n
\n {#if state === 'importing'}\n
\n \n \n
\n
\n
\n \n \n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n
\n
\n \n \n
\n {/if}\n\n {#if state === 'import_preview'}\n {#if importPreview.length > 0}\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n {#each importPreview as row}\n \n \n \n \n \n \n \n \n \n {/each}\n \n
{$t.translate?.dictionaries?.import_table_line}{$t.translate?.dictionaries?.import_table_source}{$t.translate?.dictionaries?.import_table_target}{$t.translate?.dictionaries?.import_table_src_lang}{$t.translate?.dictionaries?.import_table_tgt_lang}{$t.translate?.dictionaries?.import_table_context}{$t.translate?.dictionaries?.import_table_conflict}
{row.line}{row.source_term}{row.target_term}{row.source_language || '—'}{row.target_language || '—'}\n {#if row.has_context || row.context_data}\n {$t.translate?.dictionaries?.import_yes}\n {:else if row.usage_notes}\n {$t.translate?.dictionaries?.import_notes}\n {:else}\n —\n {/if}\n \n {#if row.is_conflict}\n \n {_('translate.dictionaries.import_existing').replace('{term}', row.existing_target_term)}\n ({importOnConflict === 'overwrite' ? $t.translate?.dictionaries?.import_will_overwrite : $t.translate?.dictionaries?.import_will_skip})\n \n {:else}\n {$t.translate?.dictionaries?.import_new}\n {/if}\n
\n
\n {/if}\n\n {#if importErrors.length > 0}\n
\n

{$t.translate?.dictionaries?.import_errors.replace('{count}', importErrors.length)}

\n {#each importErrors as err}\n

{_('translate.dictionaries.import_error_line').replace('{line}', err.line).replace('{error}', err.error)}

\n {/each}\n
\n {/if}\n\n
\n \n \n
\n {/if}\n
\n
\n\n {:else if state === 'editing'}\n {#if showAddForm}\n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n {/if}\n\n {#if entries.length === 0}\n \n
\n

{$t.translate?.dictionaries?.no_entries}

\n \n
\n
\n {:else}\n \n {#if availableSourceLanguages.length > 0 || availableTargetLanguages.length > 0}\n
\n {$t.translate?.dictionaries?.filter_by_language}\n \n \n {#each availableSourceLanguages as lang}\n \n {/each}\n \n \n \n \n {#each availableTargetLanguages as lang}\n \n {/each}\n \n {#if filterSourceLanguage || filterTargetLanguage}\n { filterSourceLanguage = ''; filterTargetLanguage = ''; }}\n class=\"text-xs text-primary hover:text-primary-hover\"\n >\n {$t.translate?.dictionaries?.filter_clear}\n \n {_('translate.dictionaries.filter_count').replace('{filtered}', filteredEntries.length).replace('{total}', entries.length)}\n {/if}\n
\n {/if}\n
\n \n \n \n \n \n \n \n \n \n \n \n {#each filteredEntries as entry}\n {#if editEntryId === entry.id}\n \n \n \n \n \n \n \n {:else}\n {\n expandedEntryId = expandedEntryId === entry.id ? null : entry.id;\n expandedEditing = false;\n if (expandedEntryId !== entry.id) {\n expandedContextData = entry.context_data ? JSON.stringify(entry.context_data, null, 2) : '';\n expandedUsageNotes = entry.usage_notes || '';\n }\n }}>\n \n \n \n \n \n \n {#if expandedEntryId === entry.id}\n \n \n \n {/if}\n {/if}\n {/each}\n \n
{$t.translate?.dictionaries?.table_source_term}{$t.translate?.dictionaries?.table_target_term}{$t.translate?.dictionaries?.table_lang}{$t.translate?.dictionaries?.table_context}{$t.translate?.dictionaries?.table_actions}
\n \n \n \n \n
\n \n \n \n
\n
\n \n \n
\n \n \n
\n
{entry.source_term}{entry.target_term}\n {#if entry.source_language || entry.target_language}\n {entry.source_language || '?'} → {entry.target_language || '?'}\n {:else}\n \n {/if}\n \n
\n {entry.context_notes || ''}\n {#if entry.has_context}\n \n context\n \n {/if}\n {#if entry.usage_notes}\n \n notes\n \n {/if}\n
\n
\n {#if deleteEntryId === entry.id}\n
\n {$t.translate?.dictionaries?.confirm_delete_entry}\n \n \n
\n {:else}\n
\n \n \n
\n {/if}\n
\n
\n {#if expandedEditing}\n \n
\n
\n Context data:\n
\n {\n try {\n const updated = await dictionaryApi.updateEntry(dictionaryId, entry.id, {\n context_data: expandedContextData ? JSON.parse(expandedContextData) : null,\n usage_notes: expandedUsageNotes,\n has_context: !!expandedContextData\n });\n addToast(_('translate.dictionaries.updated_toast'), 'success');\n expandedEditing = false;\n await loadEntries();\n } catch (e) {\n addToast(e?.message || _('translate.dictionaries.save_failed_toast'), 'error');\n }\n }}\n class=\"px-2 py-0.5 text-xs bg-primary text-white rounded hover:bg-primary-hover\"\n >{$t.translate?.dictionaries?.save_btn}\n { expandedEditing = false; }}\n class=\"px-2 py-0.5 text-xs border border-gray-300 text-gray-600 rounded hover:bg-gray-50\"\n >{$t.translate?.dictionaries?.cancel_btn}\n
\n
\n \n
\n
\n \n \n
\n {:else}\n \n {#if entry.has_context && entry.context_data}\n
\n
\n {$t.translate?.dictionaries?.context_data_label}\n {\n expandedEditing = true;\n expandedContextData = entry.context_data ? JSON.stringify(entry.context_data, null, 2) : '';\n expandedUsageNotes = entry.usage_notes || '';\n }}\n class=\"text-xs text-primary hover:text-primary-hover\"\n >{$t.translate?.dictionaries?.edit_btn}\n
\n
\n {#if typeof entry.context_data === 'object' && entry.context_data !== null}\n
\n {#each Object.entries(entry.context_data) as [key, val]}\n {key}:\n {typeof val === 'object' ? JSON.stringify(val) : String(val)}\n {/each}\n
\n {:else}\n
{JSON.stringify(entry.context_data, null, 2)}
\n {/if}\n
\n
\n {:else}\n
\n

{$t.translate?.dictionaries?.no_context_data}

\n {\n expandedEditing = true;\n expandedContextData = '';\n expandedUsageNotes = entry.usage_notes || '';\n }}\n class=\"text-xs text-primary hover:text-primary-hover\"\n >{$t.translate?.dictionaries?.add_context}\n
\n {/if}\n {#if entry.usage_notes}\n
\n {$t.translate?.dictionaries?.usage_notes_label}\n

{entry.usage_notes}

\n
\n {:else if expandedEditing}\n \n {/if}\n {/if}\n {#if entry.context_source}\n
\n {$t.translate?.dictionaries?.source_label}\n {entry.context_source}\n
\n {/if}\n
\n
\n
\n
\n\n {#if totalEntries > entryPageSize}\n
\n { entryPage--; loadEntries(); }}\n >\n {$t.translate?.dictionaries?.page_previous}\n \n \n {_('translate.dictionaries.page_info').replace('{current}', entryPage).replace('{total}', Math.ceil(totalEntries / entryPageSize))}\n \n = Math.ceil(totalEntries / entryPageSize)}\n onclick={() => { entryPage++; loadEntries(); }}\n >\n {$t.translate?.dictionaries?.page_next}\n \n
\n {/if}\n {/if}\n {/if}\n
\n\n\n" }, { "contract_id": "TranslateHistoryPage", "contract_type": "Page", "file_path": "frontend/src/routes/translate/history/+page.svelte", "start_line": 1, - "end_line": 560, + "end_line": 523, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "PURPOSE": "Page component: routes/translate/history/+page.svelte", "TYPE": "{'idle'|'loading'|'empty'|'populated'|'detail_open'|'pruned'}" }, "relations": [], @@ -102231,42 +108944,28 @@ } }, { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Page'", + "code": "invalid_enum_value", + "tag": "TYPE", + "message": "@TYPE value '{'idle'|'loading'|'empty'|'populated'|'detail_open'|'pruned'}' is not in allowed enum", "detail": { - "actual_type": "Page", - "allowed_types": [ + "allowed": [ "Module", "Function", "Class", "Component", "Block", "ADR", + "Tombstone", "Skill", "Agent" - ] + ], + "value": "{'idle'|'loading'|'empty'|'populated'|'detail_open'|'pruned'}" } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Page' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Page" - } - }, - { - "code": "unknown_tag", - "tag": "TYPE", - "message": "@TYPE is not defined in axiom_config.yaml tags", - "detail": null } ], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\t */\n\timport { onMount } from 'svelte';\n\timport { addToast } from '$lib/toasts.js';\n\timport { t, _ } from '$lib/i18n';\n\timport {\n\t\tfetchAllRuns,\n\t\tfetchRunDetail,\n\t\tfetchAllMetrics,\n\t\tfetchJobs,\n\t\tdownloadSkippedCsv,\n\t\tcancelRun,\n\t\tretryFailedBatches\n\t} from '$lib/api/translate.js';\n\n\t/** @type {'idle'|'loading'|'empty'|'populated'|'detail_open'|'pruned'} */\n\tlet uxState = $state('idle');\n\n\tlet cancellingRunId = $state(null);\n\tlet retryingRunId = $state(null);\n\n\tlet runs = $state([]);\n\tlet total = $state(0);\n\tlet currentPage = $state(1);\n\tlet pageSize = $state(20);\n\tlet selectedRun = $state(null);\n\tlet selectedRunDetail = $state(null);\n\tlet isLoading = $state(false);\n\tlet metrics = $state([]);\n\tlet jobs = $state([]);\n\n\t/** Display-friendly language names keyed by BCP-47 code */\n\tconst LANGUAGE_NAMES = {\n\t\tru: 'Русский',\n\t\ten: 'English',\n\t\tde: 'Deutsch',\n\t\tfr: 'Français',\n\t\tes: 'Español',\n\t\tit: 'Italiano',\n\t\tpt: 'Português',\n\t\tnl: 'Nederlands',\n\t\tpl: 'Polski',\n\t\tcs: 'Čeština',\n\t\tda: 'Dansk',\n\t\tfi: 'Suomi',\n\t\tsv: 'Svenska',\n\t\tnb: 'Norsk Bokmål',\n\t\tuk: 'Українська',\n\t\tja: '日本語',\n\t\tko: '한국어',\n\t\tzh: '中文',\n\t\tar: 'العربية',\n\t\ttr: 'Türkçe',\n\t\tel: 'Ελληνικά',\n\t\tro: 'Română',\n\t\thu: 'Magyar',\n\t\tvi: 'Tiếng Việt',\n\t\tth: 'ไทย',\n\t\the: 'עברית',\n\t};\n\n\tfunction getLanguageName(code) {\n\t\tconst c = String(code);\n\t\treturn LANGUAGE_NAMES[c] || (c.match(/^[a-zA-Z]{2,8}/) ? c : c);\n\t}\n\n\t// Filters\n\tlet filterJobId = $state('');\n\tlet filterStatus = $state('');\n\tlet filterTrigger = $state('');\n\tlet filterDateFrom = $state('');\n\tlet filterDateTo = $state('');\n\n\tonMount(() => {\n\t\tloadRuns();\n\t\tloadMetrics();\n\t\tloadJobs();\n\t});\n\n\tasync function loadRuns() {\n\t\tisLoading = true;\n\t\tuxState = 'loading';\n\t\ttry {\n\t\t\tconst result = await fetchAllRuns({\n\t\t\t\tpage: currentPage,\n\t\t\t\tpage_size: pageSize,\n\t\t\t\tjob_id: filterJobId || undefined,\n\t\t\t\tstatus: filterStatus || undefined,\n\t\t\t\ttrigger_type: filterTrigger || undefined,\n\t\t\t});\n\t\t\truns = result?.items || [];\n\t\t\ttotal = result?.total || 0;\n\t\t\tuxState = runs.length === 0 ? 'empty' : 'populated';\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || _('translate.history.load_failed'), 'error');\n\t\t\tuxState = 'empty';\n\t\t} finally {\n\t\t\tisLoading = false;\n\t\t}\n\t}\n\n\tasync function loadMetrics() {\n\t\ttry {\n\t\t\tmetrics = await fetchAllMetrics();\n\t\t} catch (err) {\n\t\t\t// Non-critical\n\t\t}\n\t}\n\n\tasync function loadJobs() {\n\t\ttry {\n\t\t\tconst result = await fetchJobs({ page_size: 100 });\n\t\t\tjobs = Array.isArray(result) ? result : (result?.items || result?.results || []);\n\t\t} catch (err) {\n\t\t\t// Non-critical\n\t\t}\n\t}\n\n\tasync function openDetail(run) {\n\t\tselectedRun = run;\n\t\tselectedRunDetail = null;\n\t\ttry {\n\t\t\tselectedRunDetail = await fetchRunDetail(run.id);\n\t\t\tuxState = 'detail_open';\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || _('translate.history.load_detail_failed'), 'error');\n\t\t}\n\t}\n\n\tfunction closeDetail() {\n\t\tselectedRun = null;\n\t\tselectedRunDetail = null;\n\t\tuxState = runs.length === 0 ? 'empty' : 'populated';\n\t}\n\n\tfunction applyFilters() {\n\t\tcurrentPage = 1;\n\t\tloadRuns();\n\t}\n\n\tfunction getStatusClass(status) {\n\t\tconst map = {\n\t\t\tPENDING: 'bg-yellow-100 text-yellow-700',\n\t\t\tRUNNING: 'bg-blue-100 text-blue-700',\n\t\t\tCOMPLETED: 'bg-green-100 text-green-700',\n\t\t\tFAILED: 'bg-red-100 text-red-700',\n\t\t\tCANCELLED: 'bg-gray-100 text-gray-500',\n\t\t};\n\t\treturn map[status] || 'bg-gray-100 text-gray-700';\n\t}\n\n\tfunction getJobName(jobId) {\n\t\tconst job = jobs.find(j => j.id === jobId);\n\t\treturn job ? job.name : jobId?.substring(0, 8) + '...';\n\t}\n\n\tasync function handleCancelRun(runId) {\n\t\tcancellingRunId = runId;\n\t\ttry {\n\t\t\tawait cancelRun(runId);\n\t\t\taddToast($t.translate?.run?.run_cancelled || 'Run cancelled', 'success');\n\t\t\t// Refresh the detail panel and list\n\t\t\tif (selectedRunDetail?.id === runId) {\n\t\t\t\tselectedRunDetail = await fetchRunDetail(runId);\n\t\t\t}\n\t\t\tloadRuns();\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.cancel_failed || 'Failed to cancel run', 'error');\n\t\t} finally {\n\t\t\tcancellingRunId = null;\n\t\t}\n\t}\n\n\tasync function handleRetryRun(runId) {\n\t\tretryingRunId = runId;\n\t\ttry {\n\t\t\tawait retryFailedBatches(runId);\n\t\t\taddToast($t.translate?.run?.retry_success || 'Retry started', 'success');\n\t\t\t// Refresh the detail panel and list\n\t\t\tif (selectedRunDetail?.id === runId) {\n\t\t\t\tselectedRunDetail = await fetchRunDetail(runId);\n\t\t\t}\n\t\t\tloadRuns();\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || $t.translate?.run?.retry_failed_msg || 'Retry failed', 'error');\n\t\t} finally {\n\t\t\tretryingRunId = null;\n\t\t}\n\t}\n\n\tasync function handleDownloadSkipped(runId) {\n\t\ttry {\n\t\t\tawait downloadSkippedCsv(runId);\n\t\t} catch (err) {\n\t\t\taddToast(err?.message || _('translate.history.download_failed'), 'error');\n\t\t}\n\t}\n\n\tfunction totalMetric() {\n\t\tif (!metrics.length) return null;\n\t\tconst agg = {\n\t\t\ttotal_runs: metrics.reduce((s, m) => s + (m.total_runs || 0), 0),\n\t\t\tsuccessful_runs: metrics.reduce((s, m) => s + (m.successful_runs || 0), 0),\n\t\t\tfailed_runs: metrics.reduce((s, m) => s + (m.failed_runs || 0), 0),\n\t\t\ttotal_records: metrics.reduce((s, m) => s + (m.total_records || 0), 0),\n\t\t};\n\t\treturn agg;\n\t}\n\n\n
\n\t
\n\t\t
\n\t\t\t

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

\n\t\t\t

{$t.translate?.history?.subtitle}

\n\t\t
\n\t\t\n\t
\n\n\t\n\t{#if totalMetric()}\n\t\t
\n\t\t\t
\n\t\t\t\t

{$t.translate?.history?.total_runs}

\n\t\t\t\t

{totalMetric().total_runs}

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

{$t.translate?.history?.successful}

\n\t\t\t\t

{totalMetric().successful_runs}

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

{$t.translate?.history?.failed}

\n\t\t\t\t

{totalMetric().failed_runs}

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

{$t.translate?.history?.records}

\n\t\t\t\t

{totalMetric().total_records}

\n\t\t\t
\n\t\t
\n\t{/if}\n\n\t\n\t
\n\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t
\n\t
\n\n\t\n\t{#if isLoading}\n\t\t
\n\t\t\t{#each Array(5) as _}\n\t\t\t\t
\n\t\t\t{/each}\n\t\t
\n\n\t\n\t{:else if uxState === 'empty'}\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

{$t.translate?.history?.no_runs}

\n\t\t\t

{$t.translate?.history?.no_runs_hint}

\n\t\t
\n\n\t\n\t{:else if uxState === 'populated' || uxState === 'detail_open'}\n\t\t
\n\t\t\t{#each runs as run}\n\t\t\t\t openDetail(run)}\n\t\t\t\t\tclass=\"bg-white border border-gray-200 rounded-lg p-3 hover:shadow-sm hover:border-gray-300 transition-all cursor-pointer\"\n\t\t\t\t>\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{getJobName(run.job_id)}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{run.status}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#if run.trigger_type}\n\t\t\t\t\t\t\t\t\t{run.trigger_type}\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{run.created_at ? new Date(run.created_at).toLocaleString() : ''}\n\t\t\t\t\t\t\t\t{_('translate.history.records_label').replace('{count}', run.total_records || 0)}\n\t\t\t\t\t\t\t\t{_('translate.history.ok_label').replace('{count}', run.successful_records || 0)}\n\t\t\t\t\t\t\t\t{_('translate.history.fail_label').replace('{count}', run.failed_records || 0)}\n\t\t\t\t\t\t\t\t{#if run.created_by}\n\t\t\t\t\t\t\t\t\t{_('translate.history.by_label').replace('{user}', run.created_by)}\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#if run.target_languages && run.target_languages.length > 0}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{#each run.target_languages as lang}\n\t\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
e.stopPropagation()}>\n\t\t\t\t\t\t\t{#if run.status === 'PENDING' || run.status === 'RUNNING'}\n\t\t\t\t\t\t\t\t handleCancelRun(run.id)}\n\t\t\t\t\t\t\t\t\tdisabled={cancellingRunId === run.id}\n\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 disabled:opacity-50 transition-colors\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{cancellingRunId === run.id\n\t\t\t\t\t\t\t\t\t\t? ($t.translate?.run?.cancelling || '...')\n\t\t\t\t\t\t\t\t\t\t: ($t.translate?.run?.cancel || 'Cancel')}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if run.status === 'FAILED'}\n\t\t\t\t\t\t\t\t handleRetryRun(run.id)}\n\t\t\t\t\t\t\t\t\tdisabled={retryingRunId === run.id}\n\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs bg-amber-100 text-amber-700 rounded hover:bg-amber-200 disabled:opacity-50 transition-colors\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{retryingRunId === run.id\n\t\t\t\t\t\t\t\t\t\t? ($t.translate?.run?.retrying || '...')\n\t\t\t\t\t\t\t\t\t\t: ($t.translate?.run?.retry_failed || 'Retry')}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if run.status === 'COMPLETED' && (run.skipped_records || 0) > 0}\n\t\t\t\t\t\t\t\t handleDownloadSkipped(run.id)}\n\t\t\t\t\t\t\t\t\tclass=\"text-xs text-blue-600 hover:text-blue-700\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{$t.translate?.history?.skipped_csv}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t{/each}\n\t\t
\n\n\t\t\n\t\t
\n\t\t\t

{_('translate.history.showing').replace('{count}', runs.length).replace('{total}', total)}

\n\t\t\t
\n\t\t\t\t { currentPage--; loadRuns(); }}\n\t\t\t\t\tdisabled={currentPage <= 1}\n\t\t\t\t\tclass=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50\"\n\t\t\t\t>\n\t\t\t\t\t{$t.translate?.history?.previous}\n\t\t\t\t\n\t\t\t\t { currentPage++; loadRuns(); }}\n\t\t\t\t\tdisabled={currentPage * pageSize >= total}\n\t\t\t\t\tclass=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50\"\n\t\t\t\t>\n\t\t\t\t\t{$t.translate?.history?.next}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t{/if}\n\n\t\n\t{#if uxState === 'detail_open' && selectedRunDetail}\n\t\t
\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.history?.run_detail}

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

{$t.translate?.history?.status}

\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedRunDetail.status}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.trigger}

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

{selectedRunDetail.trigger_type || 'manual'}

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

{$t.translate?.history?.started}

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

{selectedRunDetail.started_at ? new Date(selectedRunDetail.started_at).toLocaleString() : '-'}

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

{$t.translate?.history?.completed}

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

{selectedRunDetail.completed_at ? new Date(selectedRunDetail.completed_at).toLocaleString() : '-'}

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

{selectedRunDetail.total_records || 0}

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

{$t.translate?.history?.total}

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

{selectedRunDetail.successful_records || 0}

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

{$t.translate?.history?.success}

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

{selectedRunDetail.failed_records || 0}

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

{$t.translate?.history?.failed}

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

{selectedRunDetail.skipped_records || 0}

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

{$t.translate?.history?.skipped}

\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.language_stats && (Array.isArray(selectedRunDetail.language_stats) ? selectedRunDetail.language_stats.length : Object.keys(selectedRunDetail.language_stats).length) > 0}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.run?.per_language}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{#each (Array.isArray(selectedRunDetail.language_stats) ? selectedRunDetail.language_stats : Object.entries(selectedRunDetail.language_stats).map(([code, s]) => ({ ...s, language_code: code }))) as stat}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{getLanguageName(stat.language_code)}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.translated_label').replace('{count}', stat.translated_rows)}\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.failed_label').replace('{count}', stat.failed_rows)}\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.skipped_label').replace('{count}', stat.skipped_rows)}\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.tokens_label').replace('{count}', stat.token_count)}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{#if selectedRunDetail.status === 'PENDING' || selectedRunDetail.status === 'RUNNING'}\n\t\t\t\t\t\t handleCancelRun(selectedRunDetail.id)}\n\t\t\t\t\t\t\tdisabled={cancellingRunId === selectedRunDetail.id}\n\t\t\t\t\t\t\tclass=\"px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{cancellingRunId === selectedRunDetail.id\n\t\t\t\t\t\t\t\t? ($t.translate?.run?.cancelling || 'Cancelling...')\n\t\t\t\t\t\t\t\t: ($t.translate?.run?.cancel || 'Cancel')}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if selectedRunDetail.status === 'FAILED'}\n\t\t\t\t\t\t handleRetryRun(selectedRunDetail.id)}\n\t\t\t\t\t\t\tdisabled={retryingRunId === selectedRunDetail.id}\n\t\t\t\t\t\t\tclass=\"px-4 py-2 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{retryingRunId === selectedRunDetail.id\n\t\t\t\t\t\t\t\t? ($t.translate?.run?.retrying || 'Retrying...')\n\t\t\t\t\t\t\t\t: ($t.translate?.run?.retry_failed || 'Retry Failed')}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.config_snapshot}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.config_snapshot}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{JSON.stringify(selectedRunDetail.config_snapshot, null, 2)}
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.config_hash || selectedRunDetail.dict_snapshot_hash}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.snapshots}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{#if selectedRunDetail.config_hash}\n\t\t\t\t\t\t\t\t
Config: {selectedRunDetail.config_hash}
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if selectedRunDetail.dict_snapshot_hash}\n\t\t\t\t\t\t\t\t
Dict: {selectedRunDetail.dict_snapshot_hash}
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if selectedRunDetail.key_hash}\n\t\t\t\t\t\t\t\t
Key: {selectedRunDetail.key_hash}
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.events && selectedRunDetail.events.length > 0}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.events.replace('{count}', selectedRunDetail.events.length)}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{#each selectedRunDetail.events as evt}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{evt.event_type}\n\t\t\t\t\t\t\t\t\t|\n\t\t\t\t\t\t\t\t\t{evt.created_at ? new Date(evt.created_at).toLocaleString() : ''}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.error_message}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.error}

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

{selectedRunDetail.error_message}

\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.superset_execution_id}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.superset_query_id}

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

{selectedRunDetail.superset_execution_id}

\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t
\n\t\t
\n\t{/if}\n
\n\n" + "body": "\n\n\n
\n\t
\n\t\t
\n\t\t\t

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

\n\t\t\t

{$t.translate?.history?.subtitle}

\n\t\t
\n\t\t\n\t
\n\n\t\n\t{#if totalMetric()}\n\t\t
\n\t\t\t
\n\t\t\t\t

{$t.translate?.history?.total_runs}

\n\t\t\t\t

{totalMetric().total_runs}

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

{$t.translate?.history?.successful}

\n\t\t\t\t

{totalMetric().successful_runs}

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

{$t.translate?.history?.failed}

\n\t\t\t\t

{totalMetric().failed_runs}

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

{$t.translate?.history?.records}

\n\t\t\t\t

{totalMetric().total_records}

\n\t\t\t
\n\t\t
\n\t{/if}\n\n\t\n\t
\n\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t
\n\t
\n\n\t\n\t{#if isLoading}\n\t\t
\n\t\t\t{#each Array(5) as _}\n\t\t\t\t
\n\t\t\t{/each}\n\t\t
\n\n\t\n\t{:else if uxState === 'empty'}\n\t\t
\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t

{$t.translate?.history?.no_runs}

\n\t\t\t

{$t.translate?.history?.no_runs_hint}

\n\t\t
\n\n\t\n\t{:else if uxState === 'populated' || uxState === 'detail_open'}\n\t\t
\n\t\t\t{#each runs as run}\n\t\t\t\t openDetail(run)}\n\t\t\t\t\tclass=\"bg-white border border-gray-200 rounded-lg p-3 hover:shadow-sm hover:border-gray-300 transition-all cursor-pointer\"\n\t\t\t\t>\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{getJobName(run.job_id)}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{run.status}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{#if run.trigger_type}\n\t\t\t\t\t\t\t\t\t{run.trigger_type}\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{run.created_at ? new Date(run.created_at).toLocaleString() : ''}\n\t\t\t\t\t\t\t\t{_('translate.history.records_label').replace('{count}', run.total_records || 0)}\n\t\t\t\t\t\t\t\t{_('translate.history.ok_label').replace('{count}', run.successful_records || 0)}\n\t\t\t\t\t\t\t\t{_('translate.history.fail_label').replace('{count}', run.failed_records || 0)}\n\t\t\t\t\t\t\t\t{#if run.created_by}\n\t\t\t\t\t\t\t\t\t{_('translate.history.by_label').replace('{user}', run.created_by)}\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#if run.target_languages && run.target_languages.length > 0}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{#each run.target_languages as lang}\n\t\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
e.stopPropagation()}>\n\t\t\t\t\t\t\t{#if run.status === 'PENDING' || run.status === 'RUNNING'}\n\t\t\t\t\t\t\t\t handleCancelRun(run.id)}\n\t\t\t\t\t\t\t\t\tdisabled={cancellingRunId === run.id}\n\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 disabled:opacity-50 transition-colors\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{cancellingRunId === run.id\n\t\t\t\t\t\t\t\t\t\t? ($t.translate?.run?.cancelling || '...')\n\t\t\t\t\t\t\t\t\t\t: ($t.translate?.run?.cancel || 'Cancel')}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if run.status === 'FAILED'}\n\t\t\t\t\t\t\t\t handleRetryRun(run.id)}\n\t\t\t\t\t\t\t\t\tdisabled={retryingRunId === run.id}\n\t\t\t\t\t\t\t\t\tclass=\"px-2 py-1 text-xs bg-amber-100 text-amber-700 rounded hover:bg-amber-200 disabled:opacity-50 transition-colors\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{retryingRunId === run.id\n\t\t\t\t\t\t\t\t\t\t? ($t.translate?.run?.retrying || '...')\n\t\t\t\t\t\t\t\t\t\t: ($t.translate?.run?.retry_failed || 'Retry')}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if run.status === 'COMPLETED' && (run.skipped_records || 0) > 0}\n\t\t\t\t\t\t\t\t handleDownloadSkipped(run.id)}\n\t\t\t\t\t\t\t\t\tclass=\"text-xs text-primary hover:text-primary-hover\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{$t.translate?.history?.skipped_csv}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t{/each}\n\t\t
\n\n\t\t\n\t\t
\n\t\t\t

{_('translate.history.showing').replace('{count}', runs.length).replace('{total}', total)}

\n\t\t\t
\n\t\t\t\t { currentPage--; loadRuns(); }}\n\t\t\t\t\tdisabled={currentPage <= 1}\n\t\t\t\t\tclass=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50\"\n\t\t\t\t>\n\t\t\t\t\t{$t.translate?.history?.previous}\n\t\t\t\t\n\t\t\t\t { currentPage++; loadRuns(); }}\n\t\t\t\t\tdisabled={currentPage * pageSize >= total}\n\t\t\t\t\tclass=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50\"\n\t\t\t\t>\n\t\t\t\t\t{$t.translate?.history?.next}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t{/if}\n\n\t\n\t{#if uxState === 'detail_open' && selectedRunDetail}\n\t\t
\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t

{$t.translate?.history?.run_detail}

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

{$t.translate?.history?.status}

\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{selectedRunDetail.status}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.trigger}

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

{selectedRunDetail.trigger_type || 'manual'}

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

{$t.translate?.history?.started}

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

{selectedRunDetail.started_at ? new Date(selectedRunDetail.started_at).toLocaleString() : '-'}

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

{$t.translate?.history?.completed}

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

{selectedRunDetail.completed_at ? new Date(selectedRunDetail.completed_at).toLocaleString() : '-'}

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

{selectedRunDetail.total_records || 0}

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

{$t.translate?.history?.total}

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

{selectedRunDetail.successful_records || 0}

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

{$t.translate?.history?.success}

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

{selectedRunDetail.failed_records || 0}

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

{$t.translate?.history?.failed}

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

{selectedRunDetail.skipped_records || 0}

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

{$t.translate?.history?.skipped}

\n\t\t\t\t\t
\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.language_stats && Object.keys(selectedRunDetail.language_stats).length > 0}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.run?.per_language}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{#each Object.entries(selectedRunDetail.language_stats) as [lang, stats]}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.translated_label').replace('{count}', stats.translated_rows)}\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.failed_label').replace('{count}', stats.failed_rows)}\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.skipped_label').replace('{count}', stats.skipped_rows)}\n\t\t\t\t\t\t\t\t\t\t{_('translate.run.tokens_label').replace('{count}', stats.token_count)}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{#if selectedRunDetail.status === 'PENDING' || selectedRunDetail.status === 'RUNNING'}\n\t\t\t\t\t\t handleCancelRun(selectedRunDetail.id)}\n\t\t\t\t\t\t\tdisabled={cancellingRunId === selectedRunDetail.id}\n\t\t\t\t\t\t\tclass=\"px-4 py-2 text-sm bg-destructive text-white rounded-lg hover:bg-destructive-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{cancellingRunId === selectedRunDetail.id\n\t\t\t\t\t\t\t\t? ($t.translate?.run?.cancelling || 'Cancelling...')\n\t\t\t\t\t\t\t\t: ($t.translate?.run?.cancel || 'Cancel')}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if selectedRunDetail.status === 'FAILED'}\n\t\t\t\t\t\t handleRetryRun(selectedRunDetail.id)}\n\t\t\t\t\t\t\tdisabled={retryingRunId === selectedRunDetail.id}\n\t\t\t\t\t\t\tclass=\"px-4 py-2 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{retryingRunId === selectedRunDetail.id\n\t\t\t\t\t\t\t\t? ($t.translate?.run?.retrying || 'Retrying...')\n\t\t\t\t\t\t\t\t: ($t.translate?.run?.retry_failed || 'Retry Failed')}\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.config_snapshot}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.config_snapshot}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{JSON.stringify(selectedRunDetail.config_snapshot, null, 2)}
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.config_hash || selectedRunDetail.dict_snapshot_hash}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.snapshots}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{#if selectedRunDetail.config_hash}\n\t\t\t\t\t\t\t\t
Config: {selectedRunDetail.config_hash}
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if selectedRunDetail.dict_snapshot_hash}\n\t\t\t\t\t\t\t\t
Dict: {selectedRunDetail.dict_snapshot_hash}
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if selectedRunDetail.key_hash}\n\t\t\t\t\t\t\t\t
Key: {selectedRunDetail.key_hash}
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.events && selectedRunDetail.events.length > 0}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.events.replace('{count}', selectedRunDetail.events.length)}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{#each selectedRunDetail.events as evt}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{evt.event_type}\n\t\t\t\t\t\t\t\t\t|\n\t\t\t\t\t\t\t\t\t{evt.created_at ? new Date(evt.created_at).toLocaleString() : ''}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.error_message}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.error}

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

{selectedRunDetail.error_message}

\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if selectedRunDetail.superset_execution_id}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.history?.superset_query_id}

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

{selectedRunDetail.superset_execution_id}

\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t
\n\t\t
\n\t{/if}\n
\n\n" }, { "contract_id": "GitServiceContractTests", @@ -103729,6 +110428,204 @@ "tier_source": "AutoCalculated", "body": "/**\n * Delete a connection configuration.\n * @param {string} connectionId - The ID of the connection to delete.\n */" }, + { + "contract_id": "GitUtils", + "contract_type": "Module", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 1, + "end_line": 153, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Shared utility functions extracted from GitManager.svelte." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region GitUtils [C:2] [TYPE Module] [SEMANTICS git, utils, helper, normalize, parse]\n// @BRIEF Shared utility functions extracted from GitManager.svelte.\n\n/**\n * Normalize environment stage with legacy fallback.\n * Returns DEV/PREPROD/PROD.\n */\nexport function normalizeEnvStage(env) {\n if (env?.is_production) return 'PROD';\n const stage = String(env?.stage || '').trim().toUpperCase();\n if (stage === 'PROD' || stage === 'PREPROD') return stage;\n return 'DEV';\n}\n\n/**\n * Return visual class for environment stage badges.\n */\nexport function stageBadgeClass(stage) {\n if (stage === 'PROD') return 'bg-red-100 text-red-800 border-red-200';\n if (stage === 'PREPROD') return 'bg-amber-100 text-amber-800 border-amber-200';\n return 'bg-blue-100 text-blue-800 border-blue-200';\n}\n\n/**\n * Resolve active environment id for current dashboard session.\n */\nexport function resolveCurrentEnvironmentId(envId) {\n if (envId) return String(envId);\n if (typeof window === 'undefined') return null;\n return localStorage.getItem('selected_env_id');\n}\n\n/**\n * Apply GitFlow defaults by current environment stage.\n */\nexport function applyGitflowStageDefaults(stage) {\n const normalizedStage = String(stage || '').toUpperCase();\n if (normalizedStage === 'DEV') {\n return { promoteFromBranch: 'dev', promoteToBranch: 'preprod', preferredDeployTargetStage: 'PREPROD' };\n }\n if (normalizedStage === 'PREPROD') {\n return { promoteFromBranch: 'preprod', promoteToBranch: 'main', preferredDeployTargetStage: 'PROD' };\n }\n return { promoteFromBranch: 'main', promoteToBranch: 'main', preferredDeployTargetStage: '' };\n}\n\n/**\n * Checks whether current dashboard reference is numeric ID.\n */\nexport function isNumericDashboardRef(dashboardId) {\n return /^\\d+$/.test(String(dashboardId || '').trim());\n}\n\n/**\n * Return currently selected git server config.\n */\nexport function getSelectedConfig(configs, selectedConfigId) {\n return configs.find((item) => item.id === selectedConfigId) || null;\n}\n\n/**\n * Resolve default git config for current session.\n */\nexport function resolveDefaultConfig(configList, selectedConfigId) {\n if (!Array.isArray(configList) || configList.length === 0) return null;\n const selected = configList.find((item) => item.id === selectedConfigId);\n if (selected) return selected;\n const explicitDefault = configList.find((item) => item?.is_default);\n if (explicitDefault) return explicitDefault;\n const connected = configList.find((item) => item?.status === 'CONNECTED');\n return connected || configList[0];\n}\n\n/**\n * Resolve lower-case provider label for auto-push checkbox.\n */\nexport function resolvePushProviderLabel(configs, selectedConfigId, repositoryProvider) {\n const config = getSelectedConfig(configs, selectedConfigId) || resolveDefaultConfig(configs, selectedConfigId);\n const provider = String(config?.provider || repositoryProvider || '').trim().toLowerCase();\n return provider || 'git';\n}\n\n/**\n * Extract comparable host[:port] from URL string.\n */\nexport function extractHttpHost(urlValue) {\n const normalized = String(urlValue || '').trim();\n if (!normalized) return '';\n try {\n const parsed = new URL(normalized);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';\n return String(parsed.host || '').toLowerCase();\n } catch (_e) {\n return '';\n }\n}\n\n/**\n * Build deterministic repository name from dashboard title/id.\n */\nexport function buildSuggestedRepoName(dashboardTitle, dashboardId) {\n const source = (dashboardTitle || `dashboard-${dashboardId}`).toLowerCase();\n const slug = source\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48);\n return `${slug || 'dashboard'}-${dashboardId}`;\n}\n\n/**\n * Attempt to parse a JSON object from a string.\n */\nexport function tryParseJsonObject(value) {\n const source = String(value || '').trim();\n if (!source.startsWith('{') || !source.endsWith('}')) return null;\n try {\n const parsed = JSON.parse(source);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;\n } catch (_e) {\n return null;\n }\n}\n\n/**\n * Extract unfinished merge context from a 409 error.\n */\nexport function extractUnfinishedMergeContext(error) {\n if (!error || Number(error?.status) !== 409) return null;\n const parsedMessage = tryParseJsonObject(error?.message);\n const detail = error?.detail && typeof error.detail === 'object' ? error.detail : null;\n const payload = detail || parsedMessage;\n if (!payload || payload.error_code !== 'GIT_UNFINISHED_MERGE') return null;\n\n const commands = Array.isArray(payload.manual_commands)\n ? payload.manual_commands.filter(Boolean).map((item) => String(item))\n : [];\n\n const nextSteps = Array.isArray(payload.next_steps)\n ? payload.next_steps.filter(Boolean).map((item) => String(item))\n : [];\n\n return {\n message: String(payload.message || ''),\n repositoryPath: String(payload.repository_path || ''),\n gitDir: String(payload.git_dir || ''),\n currentBranch: String(payload.current_branch || ''),\n mergeHead: String(payload.merge_head || ''),\n mergeMessagePreview: String(payload.merge_message_preview || ''),\n nextSteps,\n commands,\n };\n}\n// #endregion GitUtils\n" + }, + { + "contract_id": "frontend/src/services/git-utils.js::normalizeEnvStage", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 4, + "end_line": 8, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Normalize environment stage with legacy fallback.\n * Returns DEV/PREPROD/PROD.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::stageBadgeClass", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 15, + "end_line": 18, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Return visual class for environment stage badges.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::resolveCurrentEnvironmentId", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 24, + "end_line": 27, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Resolve active environment id for current dashboard session.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::applyGitflowStageDefaults", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 33, + "end_line": 36, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Apply GitFlow defaults by current environment stage.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::isNumericDashboardRef", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 47, + "end_line": 50, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Checks whether current dashboard reference is numeric ID.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::getSelectedConfig", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 54, + "end_line": 57, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Return currently selected git server config.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::resolveDefaultConfig", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 61, + "end_line": 64, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Resolve default git config for current session.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::resolvePushProviderLabel", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 74, + "end_line": 77, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Resolve lower-case provider label for auto-push checkbox.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::extractHttpHost", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 83, + "end_line": 86, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Extract comparable host[:port] from URL string.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::buildSuggestedRepoName", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 98, + "end_line": 101, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Build deterministic repository name from dashboard title/id.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::tryParseJsonObject", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 110, + "end_line": 113, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Attempt to parse a JSON object from a string.\n */" + }, + { + "contract_id": "frontend/src/services/git-utils.js::extractUnfinishedMergeContext", + "contract_type": "Function", + "file_path": "frontend/src/services/git-utils.js", + "start_line": 124, + "end_line": 127, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Extract unfinished merge context from a 409 error.\n */" + }, { "contract_id": "StorageService", "contract_type": "Service", @@ -105160,10 +112057,19 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TIER", - "message": "@TIER is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TIER value 'TRIVIAL' is not in allowed enum", + "detail": { + "allowed": [ + "TIER_1", + "TIER_2", + "TIER_3", + "TIER_4", + "TIER_5" + ], + "value": "TRIVIAL" + } }, { "code": "def_format_deprecated", @@ -105213,10 +112119,19 @@ } }, { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TIER", - "message": "@TIER is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TIER value 'TRIVIAL' is not in allowed enum", + "detail": { + "allowed": [ + "TIER_1", + "TIER_2", + "TIER_3", + "TIER_4", + "TIER_5" + ], + "value": "TRIVIAL" + } }, { "code": "def_format_deprecated", @@ -105272,7 +112187,7 @@ "complexity": 1, "metadata": { "COMPLEXITY": "1", - "PURPOSE": "Thin wrapper — delegates to ./build.sh bundle", + "PURPOSE": "Thin wrapper — delegates to ./build.sh bundle (.tar.xz output)", "RATIONALE": "Unified into build.sh to avoid confusion; kept for backward compat." }, "relations": [], @@ -105290,7 +112205,7 @@ ], "anchor_syntax": "def", "tier_source": "AutoCalculated", - "body": "# [DEF:BuildOfflineDockerBundle:Module]\n# @PURPOSE: Thin wrapper — delegates to ./build.sh bundle\n# @COMPLEXITY: 1\n# @RATIONALE: Unified into build.sh to avoid confusion; kept for backward compat.\n# [/DEF:BuildOfflineDockerBundle:Module]\n" + "body": "# [DEF:BuildOfflineDockerBundle:Module]\n# @PURPOSE: Thin wrapper — delegates to ./build.sh bundle (.tar.xz output)\n# @COMPLEXITY: 1\n# @RATIONALE: Unified into build.sh to avoid confusion; kept for backward compat.\n# [/DEF:BuildOfflineDockerBundle:Module]\n" }, { "contract_id": "scan_secrets", @@ -105308,10 +112223,19 @@ "relations": [], "schema_warnings": [ { - "code": "unknown_tag", + "code": "invalid_enum_value", "tag": "TIER", - "message": "@TIER is not defined in axiom_config.yaml tags", - "detail": null + "message": "@TIER value 'TRIVIAL' is not in allowed enum", + "detail": { + "allowed": [ + "TIER_1", + "TIER_2", + "TIER_3", + "TIER_4", + "TIER_5" + ], + "value": "TRIVIAL" + } }, { "code": "def_format_deprecated", @@ -105360,6456 +112284,6605 @@ } ], "file_hashes": { - "venv/lib/python3.13/site-packages/pandas/tests/test_sorting.py": 10751098438703044961, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_values.py": 834951665649981397, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/METADATA": 4975169119201255384, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/subcomponents.py": 10365594934527289233, - "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.pyi": 15848834171197104301, - "venv/lib/python3.13/site-packages/pandas/util/_tester.py": 1033951890304257539, - "venv/lib/python3.13/site-packages/starlette/requests.py": 4582424111350387010, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/aioodbc.py": 2018840826888846321, - "venv/lib/python3.13/site-packages/pygments/styles/xcode.py": 14124729477757765388, - "backend/src/services/notifications/providers.py": 5844970860921555598, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_common.py": 2265736743367879051, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_category.py": 3944898528119364040, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_arithmetic.py": 17427754419626622853, - "backend/src/plugins/translate/sql_generator.py": 900251269659959067, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust.abi3.so": 2537152913308578487, - "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending_distributions.py": 209769687436320665, - "venv/lib/python3.13/site-packages/pydantic/plugin/__init__.py": 16803973597469216174, - "venv/lib/python3.13/site-packages/passlib/ext/__init__.py": 15240312484046875203, - "venv/lib/python3.13/site-packages/passlib/crypto/_md4.py": 379335134151814809, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/__init__.py": 697601512486720554, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_concat.py": 10207975532842927134, - "venv/lib/python3.13/site-packages/_pytest/fixtures.py": 1442767529726551116, - "venv/lib/python3.13/site-packages/pip/_internal/commands/list.py": 13035925292530461722, - "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/__init__.py": 18019562641348687547, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/descriptor_props.py": 7044852927393650707, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5-fcafa220.so.3.3": 5620628698712971642, - "frontend/src/components/backups/BackupList.svelte": 14706629542956875064, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_nonunique_indexes.py": 15468078561381939405, - "frontend/build/_app/immutable/chunks/Bn0PWGM8.js": 3309051336151057391, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/errors.py": 2147619440137604907, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/__init__.py": 2246704695838251846, - "venv/lib/python3.13/site-packages/numpy/_core/_simd.cpython-313-x86_64-linux-gnu.so": 12609666221355091416, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/halffloat.h": 16913381775994652343, - "venv/lib/python3.13/site-packages/pygments/styles/monokai.py": 12381683616720952636, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/x509.pyi": 17458894097215345719, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_constructors.py": 6440526981746306640, - "venv/lib/python3.13/site-packages/pandas/core/flags.py": 4690844374637430013, - "venv/lib/python3.13/site-packages/pygments/styles/rainbow_dash.py": 12236746494379891602, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE": 14610443802372107702, - "venv/lib/python3.13/site-packages/urllib3/contrib/pyopenssl.py": 11839715589709210412, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_aix.h": 13316272821279308986, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_arrow_compat.py": 17818294260710640443, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.pyi": 2700568594438441246, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_iteration.py": 15247340610412364508, - "venv/lib/python3.13/site-packages/pygments/lexers/d.py": 18120801227801223376, - "venv/lib/python3.13/site-packages/anyio/abc/_resources.py": 8330687082933028623, - "frontend/src/lib/components/dataset-review/__tests__/validation_findings_panel.ux.test.js": 8994492423600483794, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_take.py": 3281943380342853080, - "backend/src/scripts/create_admin.py": 11258281301071059367, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/parameters.py": 12219404446503940066, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_datetime.py": 7456744745424297814, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/fallback.py": 4260636031748548345, - "venv/lib/python3.13/site-packages/authlib/oidc/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/_tsql_builtins.py": 829193091321611630, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dtypes.py": 4985057130807802395, - "venv/lib/python3.13/site-packages/jose/backends/cryptography_backend.py": 2875270748627611635, - "venv/lib/python3.13/site-packages/pygments/lexers/_asy_builtins.py": 1665617945818958781, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/packaging/specifiers.py": 2800449842990194437, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/rsa.py": 5392922995791487437, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-1.csv": 16078879475693341404, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/WHEEL": 16223367184831500348, - "venv/lib/python3.13/site-packages/pandas/core/indexes/api.py": 13750417165301467381, - "venv/lib/python3.13/site-packages/jaraco/classes/properties.py": 6976928770004942858, - "venv/lib/python3.13/site-packages/attr/_compat.py": 4016698632899969390, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils_handlers.py": 556092537518149789, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_factorize.py": 861897342553280506, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_dropna.py": 3620959914439334511, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename.py": 4890779493250245903, - "venv/lib/python3.13/site-packages/requests/__init__.py": 2604491098990438229, - "venv/lib/python3.13/site-packages/PIL/PixarImagePlugin.py": 12061717157676819554, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/fft/__init__.pyi": 10354990065626209946, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/measure.py": 18327922742808984428, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_raises.py": 9678418010293693275, - "backend/src/api/routes/translate/_dictionary_routes.py": 5480137091791704976, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_period.py": 7727606662611927293, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_cell_widths.py": 5724379364993806351, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/boxplot.py": 7090586111222952862, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/authorization_server.py": 724388765673082720, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py": 1197579313959289096, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_timedelta.py": 17052461094717926538, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pct_change.py": 10313980265441433173, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/__init__.py": 6654484356035994666, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows_renderer.py": 1076842273781909680, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_astype.py": 11995587502880659624, - "venv/lib/python3.13/site-packages/pandas/core/reshape/concat.py": 2218848863149192467, - "venv/lib/python3.13/site-packages/pygments/lexers/_mql_builtins.py": 2376212048709413586, - "venv/lib/python3.13/site-packages/pandas/core/internals/api.py": 14632030170441237968, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.pyi": 2101854548535776875, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets2.py": 11100501798741918610, - "venv/lib/python3.13/site-packages/attr/setters.pyi": 6670509359139243236, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_convert.py": 17957207974799052653, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/base.py": 3250660921154829815, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/collections.py": 16114931075157411913, - "venv/lib/python3.13/site-packages/pygments/lexers/tcl.py": 10343667975436015855, - "venv/lib/python3.13/site-packages/cryptography/x509/base.py": 9124948989225782039, - "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.pyi": 11310761736182213265, - "frontend/src/components/__tests__/task_log_viewer.test.js": 6512653335179659416, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_memmap.py": 8709963808115492688, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_is_monotonic.py": 12769599198035357753, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth2_session.py": 7076806577412682835, - "venv/lib/python3.13/site-packages/pandas/io/__init__.py": 9767690163638898709, - "venv/lib/python3.13/site-packages/pydantic/v1/env_settings.py": 6194387033830177219, - "venv/lib/python3.13/site-packages/pyasn1/codec/der/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/python.py": 17886036670342116501, - "venv/lib/python3.13/site-packages/numpy/rec/__init__.py": 7467092880535460658, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_categorical.py": 9923236395692943732, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/row.py": 9763935103055608874, - "frontend/build/_app/immutable/nodes/2.BTzX8n4L.js": 10193965644456469096, - "frontend/src/components/git/CommitModal.svelte": 2248630732087966961, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_freq_attr.py": 7685756734926749093, - "venv/lib/python3.13/site-packages/anyio/streams/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tseries/api.py": 14785127136180503704, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_duplicated.py": 6659959750254523427, - "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.pyi": 6723628093359375823, - "venv/lib/python3.13/site-packages/PIL/PaletteFile.py": 4104323659569809769, - "venv/lib/python3.13/site-packages/pandas/core/arrays/numpy_.py": 16641729409992576358, - "venv/lib/python3.13/site-packages/keyring/backends/SecretService.py": 17482492072907438453, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/__init__.py": 14571251344510763303, - "venv/lib/python3.13/site-packages/pydantic/v1/config.py": 12050640083719340958, - "venv/lib/python3.13/site-packages/numpy/_core/records.pyi": 14194354215405544730, - "venv/lib/python3.13/site-packages/pytest_asyncio/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.py": 738588649412338185, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo77.f": 12000068001851270396, - "venv/lib/python3.13/site-packages/pandas/core/ops/mask_ops.py": 16152832446926420035, - "venv/bin/normalizer": 10972303485259566838, - "venv/lib/python3.13/site-packages/numpy/ma/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/core.py": 2199607970842512780, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_ufunclike.py": 13752381070039804384, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/parameters.py": 247312908510013930, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py": 16768579082462294745, - "venv/lib/python3.13/site-packages/pyasn1/type/namedtype.py": 1859990775875190116, - "backend/src/plugins/storage/plugin.py": 13669428465597027289, - "venv/lib/python3.13/site-packages/_cffi_backend.cpython-313-x86_64-linux-gnu.so": 829323374625147265, - "venv/lib/python3.13/site-packages/starlette/middleware/trustedhost.py": 8302185291610019926, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE": 10253862887434903467, - "frontend/src/lib/api/translate.js": 18119277537390809809, - "venv/lib/python3.13/site-packages/pip/_internal/req/constructors.py": 17485271035479378662, - "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_tests.cpython-313-x86_64-linux-gnu.so": 442168371609469538, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_libgroupby.py": 9985769511561272200, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.pyi": 17469792319379909312, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_count.py": 9186538441380080087, - "venv/lib/python3.13/site-packages/PIL/ImageText.py": 15322496455608242001, - "venv/lib/python3.13/site-packages/starlette/middleware/__init__.py": 9524277524025372856, - "venv/lib/python3.13/site-packages/jose/jwk.py": 2781217726458801910, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.pyi": 5973671421648618563, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_re.py": 1296579393600719224, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_deprecations.py": 8310244516175542839, - "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.cpython-313-x86_64-linux-gnu.so": 17856848989827220388, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/licenses/LICENSE": 14524289631770661180, - "venv/lib/python3.13/site-packages/urllib3/_collections.py": 3440910792774742262, - "frontend/src/routes/settings/settings-utils.js": 9490767217322820262, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/_typing/_shape.py": 12667553664122376205, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/chararray.pyi": 17340224970247832290, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_logical.py": 6818185860296232476, - "backend/src/schemas/auth.py": 339608158310320944, - "backend/src/core/task_manager/context.py": 17543992255093146942, - "backend/src/services/reports/__tests__/test_type_profiles.py": 17262223295445285285, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctanh.csv": 8620268345075425735, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_factorize.py": 5585211699185937865, - "venv/lib/python3.13/site-packages/pandas/_config/dates.py": 3863381688916127265, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/util.py": 3591114320020964752, - "venv/lib/python3.13/site-packages/pydantic/_internal/_validators.py": 2939255306814626438, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/__init__.py": 348469143777130970, - "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.py": 18402126171936308228, - "frontend/src/lib/ui/Card.svelte": 3442526978752993023, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/modules.py": 9023244176010175562, - "backend/src/api/routes/settings.py": 1899503837234642538, - "venv/lib/python3.13/site-packages/cffi/_imp_emulation.py": 17246725564704952212, - "venv/lib/python3.13/site-packages/greenlet/tests/test_weakref.py": 6780204527427742510, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/__init__.py": 11414161642984633362, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_year.py": 1478857009124807836, - "backend/src/api/routes/reports.py": 13330739376983048757, - "venv/lib/python3.13/site-packages/pandas/_testing/asserters.py": 272063856832951430, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_repr.py": 7552782621374439569, - "frontend/src/components/TaskLogViewer.svelte": 14310893311791502213, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py": 853808724844451770, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_index.py": 10971093361190283675, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpython-313-x86_64-linux-gnu.so": 7659849621107414188, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/__init__.py": 8434616194114282088, - "venv/lib/python3.13/site-packages/passlib/tests/test_win32.py": 13932956220195912963, - "venv/lib/python3.13/site-packages/pygments/styles/colorful.py": 15708848112363953394, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py": 10917162988625838981, - "frontend/src/lib/components/dataset-review/SemanticLayerReview.svelte": 2844634204872241861, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/LICENSE": 14599279238716790189, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_promote.py": 16601872688404846307, - "venv/lib/python3.13/site-packages/pandas/io/excel/_xlrd.py": 14430937200583666658, - "venv/lib/python3.13/site-packages/websockets/typing.py": 13268801935274185287, - "venv/lib/python3.13/site-packages/pydantic/v1/tools.py": 1995883197897112278, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/automap.py": 4672057253309608464, - "frontend/build/_app/immutable/chunks/4Mt7E5DY.js": 1876154373397748488, - "backend/src/core/task_manager/__tests__/test_task_logger.py": 8175976176924379623, - "venv/lib/python3.13/site-packages/pip/_internal/models/format_control.py": 12916176880659590456, - "backend/tests/services/clean_release/test_report_audit_immutability.py": 11385235469772417017, - "venv/lib/python3.13/site-packages/pandas/io/formats/xml.py": 12235904458551067063, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_regression.py": 5652583095949741238, - "venv/bin/fastapi": 12400009726251862770, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/__init__.py": 10920760520224795596, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/__main__.py": 2688850704829116818, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/__init__.py": 11709551233929312136, - "venv/lib/python3.13/site-packages/_pytest/pytester.py": 5423287282155948827, - "venv/lib/python3.13/site-packages/pandas/io/parquet.py": 14415622950469421518, - "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/okp_key.py": 10312614562914096039, - "venv/lib/python3.13/site-packages/certifi/__init__.py": 9045580998764948967, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_shift.py": 8547819448301023303, - "venv/lib/python3.13/site-packages/numpy/_core/numeric.py": 12261531924304979741, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py": 10367325397209016951, - "backend/tests/services/clean_release/test_compliance_execution_service.py": 5924200441771414883, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo90.f90": 12508663906609241323, - "venv/lib/python3.13/site-packages/pandas/core/util/numba_.py": 3535857372998653863, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_python_parser_only.py": 12231449578582108346, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/types.py": 356973910046950951, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_constructors.py": 13890224577680389718, - "venv/lib/python3.13/site-packages/uvicorn/loops/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/core/einsumfunc.py": 3484247123731380377, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/installed.py": 18153165899841229597, - "venv/lib/python3.13/site-packages/pydantic/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/utils/glibc.py": 12335359492756627313, - "venv/lib/python3.13/site-packages/ecdsa/test_keys.py": 17117188373419689467, - "venv/lib/python3.13/site-packages/jose/constants.py": 10880650415673457194, - "venv/lib/python3.13/site-packages/numpy/f2py/__main__.py": 12567751646090102167, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_errstate.py": 10788459171326750780, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/array.py": 13840510238256626632, - "venv/lib/python3.13/site-packages/pandas/core/algorithms.py": 487939626203097713, - "venv/lib/python3.13/site-packages/pygments/lexers/rita.py": 8105458520706587972, - "venv/lib/python3.13/site-packages/pygments/lexers/console.py": 18266795648198475028, - "venv/lib/python3.13/site-packages/httpx/_transports/default.py": 8391302827983907910, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/appengine.py": 16194997685865850655, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x32_unix.h": 9165722966153119887, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_arrayobject.py": 9578555381494297966, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_pipe.py": 8255917221095334212, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot_multilevel.py": 9830258607305392103, - "venv/lib/python3.13/site-packages/pycparser/_build_tables.py": 569728086093219621, - "venv/lib/python3.13/site-packages/uvicorn/_subprocess.py": 13965754246493254447, - "frontend/src/components/tools/MapperTool.svelte": 16528529568914906720, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/conftest.py": 25473177671450048, - "frontend/build/_app/immutable/nodes/11.BfFQ1EX4.js": 7853694264472099005, - "backend/src/models/storage.py": 1218824901001897736, - "frontend/src/lib/components/translate/BulkReplaceModal.svelte": 17345501275392894037, - "venv/lib/python3.13/site-packages/httpcore/_api.py": 16499985295426060327, - "venv/lib/python3.13/site-packages/sqlalchemy/util/preloaded.py": 3719702436309355562, - "venv/lib/python3.13/site-packages/smmap/test/lib.py": 2924931247676024085, - "venv/lib/python3.13/site-packages/websockets/protocol.py": 6401227903827084901, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log1p.csv": 16333455295913164408, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/dml.py": 6519732236745017861, - "venv/lib/python3.13/site-packages/pandas/core/tools/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/models/pylock.py": 16697828625021519459, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/extensions.py": 217122171232375592, - "venv/lib/python3.13/site-packages/fastapi/staticfiles.py": 4094330072433302614, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/METADATA": 11439969143853665311, - "venv/lib/python3.13/site-packages/pandas/core/strings/__init__.py": 13025387151657797130, - "venv/lib/python3.13/site-packages/PIL/DcxImagePlugin.py": 11767274218348117481, - "venv/lib/python3.13/site-packages/bcrypt/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunclike.pyi": 12215374902556647369, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/WHEEL": 13232065379159720345, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numeric.py": 5627177940393829333, - "venv/lib/python3.13/site-packages/pandas/tests/extension/json/test_json.py": 12422315190455428620, - "backend/src/plugins/__init__.py": 17194112070692432889, - "venv/lib/python3.13/site-packages/fastapi/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rank.py": 5817408252050193538, - "venv/lib/python3.13/site-packages/PIL/_deprecate.py": 15076725072094847812, - "venv/lib/python3.13/site-packages/click/formatting.py": 6694583529073729642, - "venv/lib/python3.13/site-packages/pygments/lexers/meson.py": 14659991367778746658, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.obj": 11967637259540485652, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/apply.py": 3906017176873949738, - "venv/lib/python3.13/site-packages/websockets/connection.py": 5168795926684067030, - "venv/lib/python3.13/site-packages/python_multipart/decoders.py": 14252672585219598429, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_api.py": 13319208721442328400, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_transform.py": 5674175311567817854, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/base.py": 7855217087916771270, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/models.py": 9513172366009392193, - "venv/lib/python3.13/site-packages/numpy/core/overrides.pyi": 1968648615494204783, - "venv/lib/python3.13/site-packages/pandas/core/frame.py": 2630807744775247533, - "backend/tests/services/clean_release/test_candidate_manifest_services.py": 16861589402854148894, - "venv/lib/python3.13/site-packages/numpy/fft/__init__.py": 9343449448737026658, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_online.py": 11850497659869773510, - "venv/lib/python3.13/site-packages/pandas/core/computation/common.py": 5369352684655954608, - "venv/lib/python3.13/site-packages/pandas/core/groupby/categorical.py": 16588862198627920420, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/__init__.py": 4014740126161973332, - "venv/lib/python3.13/site-packages/pandas/_libs/parsers.cpython-313-x86_64-linux-gnu.so": 14134106903573638108, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_localize.py": 14495969595744172955, - "venv/lib/python3.13/site-packages/pygments/lexers/_lasso_builtins.py": 4006621424665751347, - "backend/src/models/dataset_review_pkg/_execution_models.py": 9810012962062422666, - "venv/lib/python3.13/site-packages/PIL/_imagingmath.cpython-313-x86_64-linux-gnu.so": 77725273398858074, - "venv/lib/python3.13/site-packages/websockets/asyncio/connection.py": 17044409627448400723, - "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.pyi": 9206355685192026844, - "frontend/build/_app/immutable/nodes/16.CNhvwO2y.js": 13656477822667393544, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/converter.py": 9041278869295701566, - "venv/lib/python3.13/site-packages/numpy/random/_pcg64.cpython-313-x86_64-linux-gnu.so": 5759445712745875888, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/assignOnlyModule.f90": 15967199601254912096, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/information_schema.py": 8551227132710897346, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/RECORD": 4487477737768669516, - "venv/lib/python3.13/site-packages/_pytest/python.py": 1829073958095716309, - "venv/lib/python3.13/site-packages/pip/_internal/network/xmlrpc.py": 5064964778750366059, - "venv/lib/python3.13/site-packages/cryptography/x509/extensions.py": 10197248868682803019, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mutable.py": 9323088483998280629, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/METADATA": 10810011740984652381, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_indexing.py": 3841182456456779012, - "backend/src/schemas/translate.py": 4202862152769241553, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils_pbkdf2.py": 17016810698578473773, - "venv/lib/python3.13/site-packages/pygments/lexers/archetype.py": 8995143180482517233, - "venv/lib/python3.13/site-packages/pygments/lexers/inferno.py": 9056274496469409594, - "venv/lib/python3.13/site-packages/pandas/core/roperator.py": 2587829042468916489, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.pyi": 2884703536776830012, - "backend/src/services/dataset_review/event_logger.py": 8083530196989488371, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/asyncio.py": 11442130378027628520, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/common.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/base_server.py": 9643197259080504542, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/__init__.py": 12906995259233516197, - "venv/lib/python3.13/site-packages/referencing/_attrs.py": 14434891919747863272, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tanh.csv": 6674536825156196036, - "venv/lib/python3.13/site-packages/pyasn1/type/constraint.py": 16776615503418773283, - "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django.py": 3210697476292094384, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/result.py": 8306497889916212025, - "venv/lib/python3.13/site-packages/yaml/dumper.py": 11376022992370902766, - "venv/lib/python3.13/site-packages/pygments/styles/manni.py": 11853576691280294949, - "venv/lib/python3.13/site-packages/pygments/lexers/qlik.py": 3881663935630350152, - "backend/src/api/routes/plugins.py": 2062850550409490465, - "venv/lib/python3.13/site-packages/h11/_readers.py": 832030916375670301, - "venv/lib/python3.13/site-packages/ecdsa/test_ellipticcurve.py": 11125783981574901613, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/errors.py": 4817746830102488448, - "venv/lib/python3.13/site-packages/pandas/core/computation/pytables.py": 16326122536715961388, - "venv/lib/python3.13/site-packages/pygments/lexers/smithy.py": 10789533877863290582, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_frame.py": 4303613319747945709, - "venv/lib/python3.13/site-packages/pydantic/functional_serializers.py": 15285359935059345032, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_isin.py": 11330491145602008796, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_numpy.py": 4073641912755651877, - "venv/lib/python3.13/site-packages/fastapi/logger.py": 11590581556317053213, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/METADATA": 3786116428839780263, - "venv/lib/python3.13/site-packages/pygments/styles/arduino.py": 4578385147314541233, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/version.py": 5000350193835202949, - "venv/lib/python3.13/site-packages/numpy/doc/ufuncs.py": 11706048944901534664, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_join.py": 4213933887062089605, - "venv/lib/python3.13/site-packages/pip/_internal/network/session.py": 293771075394156394, - "frontend/build/_app/immutable/chunks/Dvp0aVhC.js": 16109929560541789895, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_asyncio.py": 2820168745710754136, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/_openssl.pyi": 4376871621615688626, - "venv/lib/python3.13/site-packages/cryptography/exceptions.py": 5173540025160237758, - "venv/lib/python3.13/site-packages/numpy/_distributor_init.py": 1101690714183863290, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/_conditional.py": 14190849361275154667, - "venv/lib/python3.13/site-packages/starlette/middleware/wsgi.py": 12506262030845301606, - "venv/lib/python3.13/site-packages/pandas/_libs/__init__.py": 17117112389651696580, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/request.py": 10492090878715126550, - "venv/lib/python3.13/site-packages/greenlet/PyModule.cpp": 3464405102256525511, - "venv/lib/python3.13/site-packages/numpy/random/__init__.pxd": 7355772669172093814, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_feather.py": 13613400750755530364, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_color.py": 3700828402084646308, - "venv/lib/python3.13/site-packages/yaml/composer.py": 13619070314468518566, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/structs.py": 4479429145468072604, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/RECORD": 963865046004908868, - "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/test_array_with_attr.py": 6568496539994758985, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_formats.py": 9738552308554950419, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_results.py": 18145930285449734975, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_matmul.py": 9323220618335072725, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/pyodbc.py": 4146960201984489604, - "backend/src/api/routes/connections.py": 3871707738958709690, - "frontend/src/routes/datasets/review/[id]/__tests__/dataset_review_workspace.ux.test.js": 4105775024258653726, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi": 10784666243744187693, - "venv/lib/python3.13/site-packages/pandas/core/sorting.py": 689057022713741051, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/secrets.py": 17816662030600756342, - "venv/lib/python3.13/site-packages/apscheduler/executors/pool.py": 10344126569001032241, - "venv/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py": 8145740626773638218, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_complex.py": 4290059580478881700, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.f90": 1249061697041863692, - "venv/lib/python3.13/site-packages/pyasn1/type/univ.py": 7163437624197683039, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.cpython-313-x86_64-linux-gnu.so": 18139875206371157315, - "venv/lib/python3.13/site-packages/pygments/styles/zenburn.py": 1031869867933871309, - "backend/src/services/clean_release/repositories/compliance_repository.py": 7402628017251283691, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pyodbc.py": 11271266527234292020, - "venv/lib/python3.13/site-packages/git/refs/head.py": 16271128287606894569, - "venv/lib/python3.13/site-packages/pygments/lexers/phix.py": 16775285097768256238, - "frontend/src/lib/ui/PageHeader.svelte": 3800784212388586861, - "venv/lib/python3.13/site-packages/pandas/tseries/offsets.py": 13750652921710295115, - "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.py": 4154852745131464543, - "backend/tests/scripts/test_clean_release_cli.py": 13978106497618764226, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asof.py": 17616601748321325187, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/accesstype.f90": 877982462186396830, - "venv/lib/python3.13/site-packages/numpy/core/__init__.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_validate.py": 12789481642730855139, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_constructors.py": 5627819885594450614, - "backend/tests/scripts/test_clean_release_tui.py": 14700450036639944764, - "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.pyi": 18193727720492050602, - "venv/lib/python3.13/site-packages/pydantic_core/__init__.py": 10563134104783445328, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py": 13215932786801020434, - "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.pyi": 6576645822227068922, - "venv/bin/pyrsa-keygen": 8222542582170363672, - "venv/lib/python3.13/site-packages/httpx/_urlparse.py": 8270294590945652412, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/RECORD": 3205266407680661753, - "venv/lib/python3.13/site-packages/pandas/_libs/tslib.pyi": 16170606956605100142, - "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.pyi": 2628035888703031029, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numeric.py": 14451517189908259173, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/groupby.py": 12749730954888633240, - "frontend/src/routes/datasets/[id]/+page.svelte": 3377235098319163133, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/METADATA": 16700674024421191168, - "venv/lib/python3.13/site-packages/pluggy/_tracing.py": 7276210024011809655, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_snap.py": 4515844385819802784, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/packages.py": 11689856177725665974, - "venv/lib/python3.13/site-packages/attr/py.typed": 15130871412783076140, - "backend/src/schemas/settings.py": 7182606947764184429, - "venv/lib/python3.13/site-packages/starlette/applications.py": 13591448461234020757, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_clip.py": 11111183698178681331, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_regression.py": 16293647824734937323, - "venv/lib/python3.13/site-packages/pandas/core/tools/datetimes.py": 13313374952742129866, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_logical_ops.py": 16166386974005224415, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_unary.py": 8046543893978198672, - "venv/lib/python3.13/site-packages/psycopg2/_ipaddress.py": 12332672835751787869, - "backend/src/api/routes/__tests__/test_assistant_authz.py": 8404416194210647280, - "venv/lib/python3.13/site-packages/numpy/_core/getlimits.pyi": 15746478550640231553, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py": 14946223589457772924, - "venv/lib/python3.13/site-packages/apscheduler/triggers/interval.py": 3964550092873794670, - "venv/lib/python3.13/site-packages/keyring/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/trafficscript.py": 776604244051744646, - "frontend/build/_app/env.js": 8815854342083926790, - "venv/lib/python3.13/site-packages/_pytest/monkeypatch.py": 2635774457434545573, - "frontend/src/components/tasks/TaskResultPanel.svelte": 193489346212840974, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_base.py": 16710163879060522196, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/metadata.py": 7204343250279221076, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py": 8659210689723088321, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_round.py": 7912161077874611432, - "venv/lib/python3.13/site-packages/jose/jwe.py": 3325660353225624025, - "venv/lib/python3.13/site-packages/ecdsa/test_eddsa.py": 1785854649532849946, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/scripts.py": 8759605303886441976, - "venv/lib/python3.13/site-packages/numpy/lib/array_utils.py": 18135114588643176359, - "venv/lib/python3.13/site-packages/websockets/legacy/__init__.py": 12332512329533591682, - "venv/lib/python3.13/site-packages/numpy/char/__init__.pyi": 12214207343146244743, - "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe.py": 16525846817331842080, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_cumulative.py": 6346947559103505068, - "venv/lib/python3.13/site-packages/pygments/lexers/_vbscript_builtins.py": 9882134558469491349, - "venv/lib/python3.13/site-packages/pygments/lexers/tal.py": 447505725492547461, - "venv/lib/python3.13/site-packages/pandas/core/col.py": 4221271461723119525, - "venv/lib/python3.13/site-packages/pygments/lexers/web.py": 11104721221615804497, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/min_max_.py": 13457611930368041821, - "venv/lib/python3.13/site-packages/greenlet/tests/test_extension_interface.py": 11693583111513457974, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_expanding.py": 9047466276603454007, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/types.py": 13384984564922916167, - "venv/lib/python3.13/site-packages/jsonschema/_format.py": 8987698648383779883, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_types.py": 651959450717569029, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_win32_console.py": 18092547253031555768, - "venv/lib/python3.13/site-packages/jose/backends/base.py": 12795468809472351388, - "venv/lib/python3.13/site-packages/rapidfuzz/__init__.py": 12462153783886855740, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_compat.h": 6512969159139431485, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_getitem.py": 11882213323878588707, - "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.pyi": 14558091964387571257, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/cli/index_command.py": 15798187258841839292, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/dtypes.py": 9839461501970023861, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py": 5467043641314706635, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_kind.py": 18178830861475480607, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_stata.py": 3030675968330529905, - "venv/lib/python3.13/site-packages/pandas/io/json/_normalize.py": 15938855324762935595, - "backend/src/services/clean_release/artifact_catalog_loader.py": 16969668877930596278, - "venv/lib/python3.13/site-packages/gitdb/base.py": 7927279446353696534, - "venv/lib/python3.13/site-packages/httpx/_auth.py": 1283189552342958900, - "venv/lib/python3.13/site-packages/pip/_internal/cli/req_command.py": 2887890799766730546, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_symbolic.py": 8093576026032854125, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/__init__.py": 3247870839323520754, - "venv/lib/python3.13/site-packages/numpy/_core/overrides.py": 252940520997718714, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_internals.py": 10472508540875131797, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_unary.py": 2573825678342936734, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed448.py": 12264234876933578367, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/urllib3/contrib/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/click/testing.py": 15973659312284678327, - "venv/lib/python3.13/site-packages/ecdsa/util.py": 10543788809724099209, - "venv/lib/python3.13/site-packages/referencing/tests/test_exceptions.py": 10846225989746274213, - "backend/src/models/filter_state.py": 6470311179852241297, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/__init__.py": 16416360448593978041, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/token.py": 6987328478153445584, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/asyncio.py": 2517623574187179319, - "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py": 17792688368296989200, - "venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py": 15244280495983216204, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/METADATA": 14180973198574731599, - "backend/src/services/clean_release/policy_resolution_service.py": 5075709738950902327, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/RECORD": 11904769847385571600, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/RECORD": 10052481303799123207, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token_validator.py": 2932866595454490527, - "venv/lib/python3.13/site-packages/keyring/testing/backend.py": 14295829193445781542, - "venv/lib/python3.13/site-packages/pygments/lexers/wren.py": 6259586056260713984, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_series_equal.py": 1223890028205011021, - "frontend/src/services/__tests__/gitService.test.js": 12845562082447336909, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_cumulative.py": 15644124412739892167, - "venv/lib/python3.13/site-packages/numpy/random/mtrand.pyi": 17144590106028661128, - "venv/lib/python3.13/site-packages/dateutil/_version.py": 8279235793001245767, - "venv/lib/python3.13/site-packages/dateutil/tz/win.py": 8427821225251397364, - "venv/lib/python3.13/site-packages/pygments/styles/vim.py": 2621036947954523349, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arithmetic.py": 14858832264660410908, - "venv/lib/python3.13/site-packages/pygments/lexers/typst.py": 8883289726601667104, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py": 5895297076117159245, - "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.py": 548928042543500365, - "venv/lib/python3.13/site-packages/numpy/random/_pickle.pyi": 16081120364372391381, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pytest/__main__.py": 8747175113746141224, - "venv/lib/python3.13/site-packages/more_itertools/more.pyi": 4844596605492825806, - "venv/lib/python3.13/site-packages/itsdangerous/_json.py": 4872267972688724005, - "venv/lib/python3.13/site-packages/dotenv/__main__.py": 16883579879172186355, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_join.py": 2585969863606902743, - "venv/lib/python3.13/site-packages/pandas/tests/config/test_config.py": 4630144191929135452, - "frontend/build/_app/immutable/chunks/DBhKLy_4.js": 87415875228682141, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/identity.py": 326059526167697655, - "backend/src/plugins/translate/_token_budget.py": 3072188939870967700, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/mod.pyi": 10591120373577657317, - "venv/lib/python3.13/site-packages/pyasn1/type/namedval.py": 8273171503516887681, - "venv/lib/python3.13/site-packages/smmap/test/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/retry.py": 17371295199208325375, - "venv/lib/python3.13/site-packages/keyring/util/__init__.py": 9635016046220094473, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_show_versions.py": 10577134135855180356, - "venv/lib/python3.13/site-packages/starlette/middleware/gzip.py": 4321371046149637632, - "venv/lib/python3.13/site-packages/anyio/abc/_streams.py": 8089057973257658396, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py": 15286893322149413712, - "venv/lib/python3.13/site-packages/fastapi/_compat/v1.py": 9776181748061371513, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_utils.pyi": 3708905060001289515, - "venv/lib/python3.13/site-packages/pandas/tests/test_aggregation.py": 16863055892192070549, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_groupby.py": 15721254196883995634, - "venv/lib/python3.13/site-packages/jsonschema/tests/_suite.py": 6307666366417438489, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_numba.py": 6409810497251251179, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/json.py": 14755215397454910426, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_half.py": 3121836756584726126, - "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/__init__.py": 63497920264089252, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_astype.py": 7497268667246913350, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/charset_normalizer/models.py": 9917300217816275821, - "venv/lib/python3.13/site-packages/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py": 14824388798676578409, - "venv/lib/python3.13/site-packages/pygments/lexers/typoscript.py": 6706632522710582762, - "backend/tests/test_log_persistence.py": 11073909507089982937, - "venv/lib/python3.13/site-packages/numpy/f2py/__version__.py": 8588600299261398598, - "venv/lib/python3.13/site-packages/pandas/io/formats/excel.py": 797619981868084011, - "venv/lib/python3.13/site-packages/pygments/lexers/ruby.py": 6898093948224302592, - "venv/lib/python3.13/site-packages/pygments/lexers/webmisc.py": 9530962803076253849, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/printing.py": 8681519649669408795, - "venv/lib/python3.13/site-packages/pygments/lexers/_lilypond_builtins.py": 1766946089314631488, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_formats.py": 18080627205081311773, - "backend/tests/test_translate_executor_filter.py": 11717560360659156703, - "venv/lib/python3.13/site-packages/pyasn1/codec/native/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/pip/_internal/models/selection_prefs.py": 14700113736620314923, - "venv/lib/python3.13/site-packages/h11/_abnf.py": 10456996134895429201, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/request.py": 10484280068366411541, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/gh_22819.pyf": 14717748358749934995, - "venv/lib/python3.13/site-packages/pip/_internal/commands/freeze.py": 8644678595442275485, - "venv/lib/python3.13/site-packages/starlette/templating.py": 9242638124487797988, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.pyi": 17528421279543420867, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_tools.py": 13994164937603424236, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_nonkeyword_arguments.py": 11676809974120007408, - "venv/lib/python3.13/site-packages/PIL/PdfParser.py": 13710342183037972042, - "venv/lib/python3.13/site-packages/yaml/emitter.py": 6036657460416912844, - "backend/src/schemas/health.py": 13764741380188041840, - "venv/lib/python3.13/site-packages/pygments/lexers/fantom.py": 1986459352773446836, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/char.f90": 12203871998988759364, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_delete.py": 5958440386632506587, - "venv/lib/python3.13/site-packages/pandas/arrays/__init__.py": 10005095663840461954, - "backend/src/services/clean_release/exceptions.py": 14631509303697939633, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_has_cy.py": 11979732417254560424, - "venv/lib/python3.13/site-packages/uvicorn/middleware/asgi2.py": 5355343704425405009, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/_csound_builtins.py": 14530739818491398298, - "backend/src/api/routes/git/_repo_operations_routes.py": 17252968422647919723, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_globals.py": 8736093011628027941, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/extension_types.py": 1856757457657310231, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ma.py": 15912582628112164509, - "venv/lib/python3.13/site-packages/pygments/lexers/factor.py": 15788613154609197002, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_ranges.py": 2572903460327860111, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/RECORD": 1556051687830913019, - "backend/src/services/git/_sync.py": 16596597562042405179, - "venv/lib/python3.13/site-packages/gitdb/test/lib.py": 13294941660283059872, - "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py": 14282751162289385516, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh15035.f": 13126319439858352711, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta_range.py": 15081095442418142820, - "venv/lib/python3.13/site-packages/pygments/styles/sas.py": 12363791134941577382, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/h11/_state.py": 11398755254809751139, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_arithmetic.py": 16045758521544316139, - "venv/lib/python3.13/site-packages/cffi/vengine_cpy.py": 10684192171370688079, - "venv/lib/python3.13/site-packages/pip/_internal/commands/hash.py": 11824898770873127699, - "venv/lib/python3.13/site-packages/gitdb/fun.py": 55226330810782184, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py": 7947998260347730581, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/win64python2.npy": 11005857803794805939, - "backend/src/scripts/clean_release_cli.py": 14467814161661747234, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite.py": 3628609433179387988, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarinherit.py": 15240535501243623986, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/METADATA": 11557342107649194148, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked_shared.py": 8453174300922339015, - "venv/lib/python3.13/site-packages/pluggy/_version.py": 7195758686717994475, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.py": 5564496700647900501, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/bulk_persistence.py": 2307818727114646866, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_comparison.py": 11333735187162513512, - "venv/lib/python3.13/site-packages/pygments/styles/fruity.py": 7925019839136894457, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/var_.py": 12699521091170347311, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dlpack.py": 7908102652282922952, - "venv/lib/python3.13/site-packages/passlib/tests/sample1b.cfg": 2872366861156549016, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi": 16312114056447366970, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_string.py": 10141066226714805009, - "venv/lib/python3.13/site-packages/git/objects/submodule/root.py": 5322248117564025370, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/RECORD": 4857922668040624663, - "venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py": 9639351759822608724, - "venv/lib/python3.13/site-packages/pygments/lexers/soong.py": 10273453675034306814, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_setops.py": 15196593522537569500, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py": 8859757563354666575, - "venv/lib/python3.13/site-packages/PIL/ImageDraw.py": 12919304671639673744, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_join.py": 17874591841487372992, - "venv/lib/python3.13/site-packages/pygments/lexers/_sourcemod_builtins.py": 11809503654078535014, - "venv/lib/python3.13/site-packages/greenlet/TBrokenGreenlet.cpp": 10822436772777706192, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/context.py": 1041624619197012753, - "venv/lib/python3.13/site-packages/pandas/core/reshape/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_constructors.py": 9815164146739254076, - "venv/lib/python3.13/site-packages/pandas/_libs/sas.cpython-313-x86_64-linux-gnu.so": 16309838137741124637, - "venv/lib/python3.13/site-packages/pandas/io/excel/_xlsxwriter.py": 16873939324418752965, - "venv/lib/python3.13/site-packages/git/objects/commit.py": 12317865358846331802, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fromnumeric.pyi": 8671744104085911610, - "venv/lib/python3.13/site-packages/pandas/io/formats/format.py": 3364941338174434661, - "venv/lib/python3.13/site-packages/pandas/core/window/common.py": 17956497339875932377, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_old_base.py": 191359377446257900, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_from_scalar.py": 16520136036631155842, - "venv/lib/python3.13/site-packages/pip/_internal/configuration.py": 2563100675379605810, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas7bdat.py": 8963983384208121526, - "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.pyi": 16573082220135508510, - "venv/lib/python3.13/site-packages/pygments/style.py": 17393199235575070210, - "frontend/build/_app/immutable/nodes/19.UaiNjUdw.js": 17580400751027983788, - "venv/lib/python3.13/site-packages/pygments/lexers/pddl.py": 9256112535059995841, - "venv/lib/python3.13/site-packages/pandas/core/arrays/boolean.py": 8821671562937866269, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_multiarray.py": 3592197154340095431, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_day.py": 2200008998040663785, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/token_validator.py": 13917237710752731397, - "venv/lib/python3.13/site-packages/urllib3/http2/connection.py": 1661620860881397554, - "frontend/src/lib/stores/__tests__/setupTests.js": 13316015450930652502, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/libdivide.h": 5314232500579481313, - "venv/lib/python3.13/site-packages/pygments/lexers/crystal.py": 3579547906957125755, - "venv/lib/python3.13/site-packages/pandas/core/ops/missing.py": 9493665944764255854, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_convert.py": 77283525137564255, - "venv/lib/python3.13/site-packages/pygments/lexers/agile.py": 5686162551416231457, - "backend/src/api/routes/admin.py": 16164005699911103765, - "venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py": 8657533190026387896, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_api.py": 7131036004750554905, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py": 13454394827268856575, - "venv/lib/python3.13/site-packages/fastapi/websockets.py": 9910052969532075719, - "venv/lib/python3.13/site-packages/_pytest/assertion/truncate.py": 14000392306994773741, - "frontend/src/routes/translate/history/+page.svelte": 7874402226825627914, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.cpython-313-x86_64-linux-gnu.so": 6726636942028566415, - "venv/lib/python3.13/site-packages/numpy/linalg/__init__.py": 15647756307608142577, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/__init__.py": 2436195108273216959, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_frame.py": 2169381747938271444, - "venv/lib/python3.13/site-packages/rsa/prime.py": 10820898885044917095, - "venv/lib/python3.13/site-packages/jsonschema/exceptions.py": 15651767069704734674, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/pkg_resources.py": 14271077832999370068, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/licenses/LICENSE": 13322127602069971876, - "venv/lib/python3.13/site-packages/pillow.libs/libharfbuzz-0692f733.so.0.61230.0": 23110287346167671, - "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.py": 14696841109142419143, - "venv/lib/python3.13/site-packages/numpy/random/_mt19937.cpython-313-x86_64-linux-gnu.so": 85866616759631810, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_editable.py": 14352954849821931255, - "venv/lib/python3.13/site-packages/pydantic/root_model.py": 20625057828723317, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/jwe.py": 10478268710546063196, - "venv/lib/python3.13/site-packages/pip/_internal/commands/download.py": 7533669665002975823, - "frontend/build/_app/immutable/nodes/34.CPCAjONs.js": 8672599980797084253, - "venv/lib/python3.13/site-packages/fastapi/middleware/cors.py": 7452860773712601508, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/METADATA": 11419135077900073883, - "venv/lib/python3.13/site-packages/pandas/core/indexes/frozen.py": 1419685664276640120, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_gcs.py": 7733247456252965901, - "venv/lib/python3.13/site-packages/pip/_internal/utils/_jaraco_text.py": 9046755357011730699, - "venv/lib/python3.13/site-packages/jeepney/fds.py": 16573439672758641560, - "venv/lib/python3.13/site-packages/pydantic/v1/version.py": 2758971830527526972, - "frontend/build/_app/immutable/chunks/ikwS1dR6.js": 3267599179274813039, - "venv/lib/python3.13/site-packages/pydantic/type_adapter.py": 10162988530225478692, - "venv/lib/python3.13/site-packages/pygments/styles/nord.py": 8469538903422182155, - "backend/src/core/auth/logger.py": 14199514105394646252, - "venv/lib/python3.13/site-packages/rapidfuzz/process_py.py": 17563673072076876125, - "venv/lib/python3.13/site-packages/pydantic/json_schema.py": 15678278224696353978, - "venv/lib/python3.13/site-packages/pip/_internal/operations/freeze.py": 3469845570614985559, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_typing.py": 17589387690638097975, - "venv/lib/python3.13/site-packages/httpcore/_backends/anyio.py": 6167729964577005634, - "venv/lib/python3.13/site-packages/pygments/lexers/rust.py": 8605690716125176329, - "venv/lib/python3.13/site-packages/numpy/fft/tests/test_pocketfft.py": 18025182347224285050, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_take.py": 1216902953519121176, - "backend/src/core/task_manager/persistence.py": 7896628499516364250, - "venv/lib/python3.13/site-packages/numpy/matlib.py": 112839038090033740, - "venv/lib/python3.13/site-packages/cffi/ffiplatform.py": 6782148110863549472, - "venv/lib/python3.13/site-packages/passlib/utils/__init__.py": 11704122205941631503, - "frontend/src/lib/stores/health.js": 3437957055218721782, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/RECORD": 11111103193315065053, - "venv/lib/python3.13/site-packages/starlette/config.py": 16509394679506300256, - "venv/lib/python3.13/site-packages/sqlalchemy/inspection.py": 8563914460959280101, - "backend/src/core/auth/security.py": 8289703096420999159, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/util.py": 18279366777506483176, - "venv/lib/python3.13/site-packages/python_multipart/__init__.py": 10290895261363712562, - "venv/lib/python3.13/site-packages/pandas/core/groupby/indexing.py": 18009437216172136227, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py": 8530403239168529668, - "venv/lib/python3.13/site-packages/_pytest/python_api.py": 12953851690889336092, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem": 15624463015628939254, - "venv/lib/python3.13/site-packages/pygments/lexers/foxpro.py": 14032656072384708293, - "venv/lib/python3.13/site-packages/pygments/styles/tango.py": 12389250878249075539, - "backend/src/api/routes/assistant/_command_parser.py": 2745692387994130159, - "frontend/src/lib/toasts.js": 199299975383904134, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_nlargest.py": 1423106021399469694, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/errors.py": 17581857895562494222, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_object_arr.py": 4725156416161661681, - "venv/lib/python3.13/site-packages/pygments/lexers/blueprint.py": 6370363226580199085, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/__init__.py": 15130871412783076140, - "backend/src/app.py": 1030174853208129525, - "venv/bin/f2py": 11006035631504708368, - "frontend/src/components/MissingMappingModal.svelte": 11264881226066623003, - "venv/lib/python3.13/site-packages/anyio/_core/_exceptions.py": 11969894261631227715, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_to_xarray.py": 15341013524814290960, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_deprecations.py": 6015734648984591027, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_iceberg.py": 1726421917529457420, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_formats.py": 17821648708413143557, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_aarch64_gcc.h": 8140470539757491899, - "venv/lib/python3.13/site-packages/pandas/core/reshape/melt.py": 14109947554124551986, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py": 9964642152916165317, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_select.py": 7083040959665006634, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_polynomial.py": 3049262771098624825, - "venv/lib/python3.13/site-packages/psycopg2/_range.py": 7184417109068014771, - "venv/lib/python3.13/site-packages/numpy/ma/mrecords.py": 687170905107114309, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.py": 2401931196871484449, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timezones.py": 1962830142478494009, - "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.pyi": 8879196542984811747, - "venv/lib/python3.13/site-packages/sqlalchemy/util/typing.py": 17638587764345605306, - "venv/lib/python3.13/site-packages/uvicorn/workers.py": 7213290478118197047, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py": 11808051681178127650, - "venv/lib/python3.13/site-packages/pygments/lexers/capnproto.py": 15713281507099411738, - "frontend/src/components/llm/DocPreview.svelte": 9286231407313632900, - "frontend/build/_app/immutable/chunks/BsdYA8vJ.js": 17313445894049649179, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/RECORD": 9974324769263969827, - "venv/lib/python3.13/site-packages/pygments/lexers/vip.py": 11287963054551122004, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/METADATA": 13633829318759752611, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/__init__.py": 16123561007855424016, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_infer_objects.py": 16868563755487479303, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_localize.py": 4937713043686953098, - "frontend/src/pages/Settings.svelte": 5110524159352803966, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategies.py": 15673926142454922581, - "venv/lib/python3.13/site-packages/pandas/_config/__init__.py": 3577026529387563519, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/matrix.pyi": 15667024840812783911, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.cpython-313-x86_64-linux-gnu.so": 6011605826065363427, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/WHEEL": 7145482834744213753, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/__init__.pyi": 15820654525292895396, - "venv/lib/python3.13/site-packages/starlette/responses.py": 14081178833432955042, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi": 11455144434558323236, - "venv/lib/python3.13/site-packages/pydantic/v1/networks.py": 13262073155118055511, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/PIL/PdfImagePlugin.py": 16082854250442815862, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_from_dummies.py": 5670811108950300761, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/utils.py": 2959395751217827103, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_orc.py": 1248059610437937895, - "venv/lib/python3.13/site-packages/pip/_internal/models/link.py": 7172346414535657925, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/markers.py": 8297667990263004097, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/quantile.py": 17341029201525362074, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/__init__.py": 1856412417337875350, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_argon2.py": 11345706437113628133, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.cpython-313-x86_64-linux-gnu.so": 17089839649971421164, - "venv/lib/python3.13/site-packages/attr/setters.py": 12467854389745942025, - "venv/lib/python3.13/site-packages/pandas/_libs/_cyutility.cpython-313-x86_64-linux-gnu.so": 3372660143512868174, - "venv/lib/python3.13/site-packages/websockets/frames.py": 4901850477414613953, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_monotonic.py": 14648437502004398715, - "venv/lib/python3.13/site-packages/numpy/f2py/rules.pyi": 1342380029484127983, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_expanding.py": 12504275097476819918, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data": 11957232619107868023, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA_py.py": 2494967094400350388, - "venv/lib/python3.13/site-packages/numpy/_core/memmap.py": 4093510602708416858, - "venv/lib/python3.13/site-packages/annotated_types/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/jeepney/tests/test_bindgen.py": 8637878018232418583, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598.f90": 1409455010615482779, - "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_dict.py": 4937173601709369119, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_multi_thread.py": 5660352470978621226, - "venv/lib/python3.13/site-packages/urllib3/util/proxy.py": 17667887856427739069, - "frontend/src/routes/datasets/review/+page.svelte": 1567550142023887137, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filters/__init__.py": 835924536855006596, - "venv/lib/python3.13/site-packages/click/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/ma/__init__.pyi": 1855615156913010107, - "venv/lib/python3.13/site-packages/pillow.libs/libwebpdemux-747f2b49.so.2.0.17": 7615380692384566654, - "venv/lib/python3.13/site-packages/fastapi/temp_pydantic_v1_params.py": 9436036880518047720, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh17859.f": 13562390882286135024, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string_arrow.py": 10008395772220468318, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.py": 4672871351134074735, - "venv/lib/python3.13/site-packages/git/config.py": 1724894144987553201, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args_and_kwargs.py": 3381939712123764154, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/PIL/BufrStubImagePlugin.py": 5520941821175895889, - "venv/lib/python3.13/site-packages/pygments/styles/abap.py": 11543320440974964547, - "backend/src/core/config_manager.py": 18161236261317111528, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_free.f90": 8821492401453445757, - "backend/src/services/clean_release/stages/internal_sources_only.py": 3635582838654262092, - "backend/logs/app.log.bak": 12967408684782809237, - "backend/src/plugins/llm_analysis/service.py": 6500458189357690080, - "backend/alembic/versions/c4a3a2f74bfe_add_missing_columns_needs_review_.py": 4320606555167902916, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/memmap.pyi": 2834143910417841603, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/flatiter.pyi": 6885824496927786205, - "venv/lib/python3.13/site-packages/attr/_cmp.py": 3297594878548179300, - "venv/lib/python3.13/site-packages/smmap/__init__.py": 4557520096828426601, - "venv/lib/python3.13/site-packages/websockets/cli.py": 14049106025886056110, - "venv/lib/python3.13/site-packages/pydantic/v1/__init__.py": 1642038155370734658, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-expm1.csv": 8710594949902856122, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_query_eval.py": 17613800806748649144, - "venv/lib/python3.13/site-packages/pip/_internal/cli/status_codes.py": 9143625681947330756, - "venv/lib/python3.13/site-packages/_pytest/config/findpaths.py": 7867603756389940562, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/_typing.py": 10524021526716767489, - "venv/lib/python3.13/site-packages/fastapi/types.py": 3867821246650502267, - "venv/lib/python3.13/site-packages/cffi/cparser.py": 5246372077864988755, - "venv/lib/python3.13/site-packages/numpy/random/_philox.cpython-313-x86_64-linux-gnu.so": 9916143171057774705, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py": 2833016678144975576, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_alpha_unix.h": 2759424570606117942, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_arrow_string_mixins.py": 16862663427780315543, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iloc.py": 7586049606891014270, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_gcc.h": 3561524604274567339, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/binding.py": 17078280153372356796, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_abc.py": 10164280045317807365, - "venv/lib/python3.13/site-packages/pip/py.typed": 10243553515949422671, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_round_trip.py": 14684057343786032776, - "venv/lib/python3.13/site-packages/sqlalchemy/util/deprecations.py": 7020540470348910770, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/path_registry.py": 8410829659513707067, - "venv/lib/python3.13/site-packages/jaraco/classes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/python_multipart/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/__init__.pyi": 10548370574281981402, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_dtype.py": 1647214604678171565, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_coercion.py": 3339075591248270762, - "venv/lib/python3.13/site-packages/attrs/validators.py": 10346563361083429998, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_keys.py": 10813898277355150779, - "venv/lib/python3.13/site-packages/anyio/_core/_testing.py": 14024940365851109155, - "venv/lib/python3.13/site-packages/pydantic_settings/exceptions.py": 18300876747412267999, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_codes.py": 13901854719435716195, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/generate_umath_validation_data.cpp": 10880681943327843928, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_constructors.py": 16197656939536784142, - "venv/lib/python3.13/site-packages/greenlet/__init__.py": 3447613556981242475, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_autocorr.py": 1092316868398391792, - "venv/lib/python3.13/site-packages/urllib3/util/ssl_match_hostname.py": 7411350247004265632, - "venv/lib/python3.13/site-packages/pygments/lexers/_usd_builtins.py": 8671446144574174964, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_macos.py": 7028037591427052891, - "venv/lib/python3.13/site-packages/numpy/lib/user_array.pyi": 1968484998382771986, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sinh.csv": 12358602501260667480, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py": 9224234539232195189, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray_misc.pyi": 16219573290116396099, - "venv/lib/python3.13/site-packages/pandas/tests/apply/conftest.py": 13798335354961261346, - "venv/lib/python3.13/site-packages/pygments/lexers/apdlexer.py": 14222752852221009085, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/package_data.py": 3078502124870585867, - "venv/lib/python3.13/site-packages/numpy/_core/tests/_locales.py": 18178500267053825979, - "venv/lib/python3.13/site-packages/pandas/_testing/contexts.py": 11714205921752896137, - "venv/lib/python3.13/site-packages/pygments/lexers/_stata_builtins.py": 18053452765493556047, - "venv/lib/python3.13/site-packages/requests/certs.py": 16052880877156864882, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp2.csv": 10950201682596097452, - "venv/lib/python3.13/site-packages/uvicorn/__main__.py": 6086938803696646644, - "venv/lib/python3.13/site-packages/cryptography/__about__.py": 10471312538528457144, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE": 14316979437290727897, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.pyf": 1361426175133186231, - "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.pyi": 8708830448398500675, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi": 9405122752084210696, - "venv/lib/python3.13/site-packages/pandas/core/computation/api.py": 17919983681396223380, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_index.py": 13517883160584591966, - "venv/lib/python3.13/site-packages/httpcore/_ssl.py": 6665087294869504820, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_internal/utils/compat.py": 1841193561752758336, - "frontend/src/lib/components/layout/sidebarNavigation.js": 12067620255981492739, - "backend/src/api/routes/dashboards/_detail_routes.py": 12522696473630508473, - "backend/src/api/routes/clean_release_v2.py": 2858037566596334612, - "venv/lib/python3.13/site-packages/numpy/lib/scimath.pyi": 18165025261847276375, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_pyf_src.py": 12351476647369971852, - "venv/lib/python3.13/site-packages/packaging/tags.py": 1999137344425005827, - "frontend/src/components/EnvSelector.svelte": 3937920133889630193, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/decl_class.py": 6328411894571735935, - "venv/lib/python3.13/site-packages/pygments/lexers/asn1.py": 15030988816882821134, - "frontend/build/_app/immutable/nodes/36.DZAutSCl.js": 9000473499840988160, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/rsa.py": 7852235015155694076, - "venv/lib/python3.13/site-packages/attr/converters.py": 5179615793507170642, - "venv/lib/python3.13/site-packages/passlib/utils/md4.py": 4521366665754877351, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_shift.py": 14681514853406253657, - "backend/src/scripts/test_dataset_dashboard_relations.py": 6373309008834670516, - "backend/src/api/routes/git/_helpers.py": 6888678759460652166, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_version.py": 15336513445621042105, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_reshape.py": 5572426834735088916, - "venv/lib/python3.13/site-packages/pandas/tests/test_col.py": 14781555956509956371, - "venv/lib/python3.13/site-packages/PIL/__main__.py": 827665918723937817, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py": 10455718110956856820, - "venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py": 932468678264370661, - "backend/src/models/assistant.py": 6168086159312177112, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/errors.py": 10750605030539582738, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/WHEEL": 13347410390513723930, - "venv/lib/python3.13/site-packages/pydantic/v1/dataclasses.py": 7833468513057242440, - "venv/lib/python3.13/site-packages/jose/backends/rsa_backend.py": 13507186353881931758, - "venv/lib/python3.13/site-packages/pygments/styles/stata_light.py": 16319330015324455277, - "venv/lib/python3.13/site-packages/pydantic_settings/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_trio.py": 7459901627473962540, - "venv/lib/python3.13/site-packages/pip/_internal/utils/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.c": 2519219289444051739, - "venv/lib/python3.13/site-packages/pandas/io/sas/sas_constants.py": 14492427939536692313, - "venv/lib/python3.13/site-packages/apscheduler/executors/base.py": 1980520974511822101, - "venv/lib/python3.13/site-packages/passlib/tests/test_context_deprecated.py": 7876919198015151018, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/RECORD": 9502524172853437600, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_upcast.py": 6496794053681247105, - "venv/lib/python3.13/site-packages/rapidfuzz/utils_cpp.cpython-313-x86_64-linux-gnu.so": 16145829319198889672, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/sessions.py": 5400174351499383072, - "venv/lib/python3.13/site-packages/_pytest/skipping.py": 10561585274103209115, - "venv/lib/python3.13/site-packages/pydantic/_internal/_generate_schema.py": 7343380095251236503, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/RECORD": 7844752443022514265, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/create.py": 16693261531832306870, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_shape_manipulation.py": 12598431412396627042, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_character.py": 17168707712220699738, - "venv/lib/python3.13/site-packages/PIL/ImageOps.py": 3884310863548175682, - "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.pyi": 9291444594181042963, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/conftest.py": 8406022190801970256, - "venv/lib/python3.13/site-packages/pygments/lexers/func.py": 12257919753150082714, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_threading.py": 6656552495241857519, - "backend/src/core/__tests__/test_throttled_scheduler.py": 10949494963069000037, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py": 5616705208267050659, - "venv/lib/python3.13/site-packages/starlette/testclient.py": 16088848330558215194, - "backend/src/services/__tests__/test_resource_service.py": 14435810729789665272, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x448.pyi": 11733449165276720901, - "backend/src/plugins/backup.py": 9697280948668596192, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/licenses/LICENSE": 3868190977070717994, - "venv/lib/python3.13/site-packages/pygments/lexers/haxe.py": 1915270891525929751, - "backend/src/services/notifications/__tests__/test_notification_service.py": 2377464079178202565, - "backend/tests/test_models.py": 3998442321184404421, - "venv/lib/python3.13/site-packages/numpy/core/multiarray.py": 18368443598659711836, - "venv/lib/python3.13/site-packages/_pytest/threadexception.py": 10464996327276961035, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_regression.py": 4611768764484593979, - "venv/lib/python3.13/site-packages/rsa/util.py": 13423880475585138638, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_amd64_unix.h": 16679226724722970205, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_item.py": 17619314877770917080, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_index_equal.py": 10617870815989304510, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pop.py": 5700439923973346681, - "backend/src/services/clean_release/__tests__/test_audit_service.py": 1679130450221264383, - "venv/lib/python3.13/site-packages/pandas/core/_numba/executor.py": 2205130934861422769, - "venv/lib/python3.13/site-packages/_pytest/tracemalloc.py": 5530234062530858720, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/datetimelike_accumulations.py": 14719894187829729175, - "venv/lib/python3.13/site-packages/pandas/core/ops/invalid.py": 10186546927135277579, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_constructors.py": 12118296571694122743, - "venv/lib/python3.13/site-packages/pip/_internal/models/index.py": 2839270935469791607, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_reductions.py": 17810094294132819208, - "backend/src/services/__init__.py": 15123879863527585264, - "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.pyi": 14181518821845356636, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/exceptions.py": 2873060848310644552, - "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.pyi": 15745512451486888537, - "venv/lib/python3.13/site-packages/httpcore/_async/http11.py": 7565741048880549265, - "venv/lib/python3.13/site-packages/pandas/core/arrays/timedeltas.py": 7318851108932357510, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/inference.py": 2786441797589123900, - "frontend/src/routes/storage/backups/+page.svelte": 3437582895363203695, - "backend/src/models/dataset_review_pkg/_session_models.py": 14346374229033337012, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_like.py": 2778905818573625118, - "venv/lib/python3.13/site-packages/websockets/uri.py": 264376168984862773, - "frontend/src/routes/admin/roles/+page.svelte": 12213033002088655108, - "venv/lib/python3.13/site-packages/authlib/jose/jwk.py": 13560749074922587626, - "backend/tests/test_task_manager.py": 1706217361689506763, - "venv/lib/python3.13/site-packages/pip/_internal/commands/uninstall.py": 11003472265011262515, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/RECORD": 6488682447051657004, - "venv/lib/python3.13/site-packages/greenlet/tests/test_stack_saved.py": 7767924647054186572, - "venv/lib/python3.13/site-packages/pandas/api/__init__.py": 1145864207387918302, - "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.py": 12771179468704314203, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_pad.pyi": 14755435694146003896, - "frontend/src/lib/stores/assistantChat.js": 13807808925025688939, - "frontend/src/routes/+error.svelte": 16649211425103750277, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/datasource.pyi": 16752667877866430577, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_period.py": 3419297908709788013, - "venv/lib/python3.13/site-packages/pip/_internal/index/package_finder.py": 12631116673181479779, - "venv/lib/python3.13/site-packages/referencing/tests/test_core.py": 8280983300775878144, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_asof.py": 18383318159051131669, - "venv/lib/python3.13/site-packages/ecdsa/curves.py": 812969717084691115, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth2_client.py": 6742098857947604729, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/cookies.py": 4997226307650350876, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/sync.py": 6189260178302531376, - "venv/lib/python3.13/site-packages/pygments/lexers/spice.py": 1376126320935115350, - "venv/lib/python3.13/site-packages/packaging/markers.py": 11602308203217831543, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo90.f90": 3492659529812762861, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/fixed_string.f90": 8654788371538544793, - "frontend/src/routes/settings/EnvironmentsTab.svelte": 16246319452479236452, - "venv/lib/python3.13/site-packages/smmap/test/test_mman.py": 8977119225749253844, - "backend/src/models/dataset_review_pkg/__init__.py": 17789694606496652579, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/__init__.py": 4770885427721822507, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x25519.py": 18229054440703471493, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray.pyi": 14369009813591437391, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/provision.py": 17812733620485633975, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/socks.py": 17581293673028078134, - "venv/lib/python3.13/site-packages/httpcore/_backends/trio.py": 8394426709857317737, - "venv/lib/python3.13/site-packages/uvicorn/importer.py": 5449629250971153435, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_editable.py": 12494407279914732338, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_cross.py": 17742712348672218852, - "venv/lib/python3.13/site-packages/pygments/lexers/_cocoa_builtins.py": 15468347167912293394, - "backend/tests/services/clean_release/test_publication_service.py": 17277940125978730159, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_crosstab.py": 6631711416347033281, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_spss.py": 4865360315909532603, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_map.py": 4761668276463940924, - "venv/lib/python3.13/site-packages/passlib/tests/test_registry.py": 15377817946543064293, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_style.py": 6849613206971398029, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/licenses/LICENSE": 1424673738228762254, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply_relabeling.py": 5600949862632014700, - "frontend/src/components/tasks/TaskLogPanel.svelte": 7807716346006847037, - "venv/lib/python3.13/site-packages/pandas/core/generic.py": 10174952306641057447, - "venv/lib/python3.13/site-packages/pygments/token.py": 6987328478153445584, - "venv/lib/python3.13/site-packages/pandas/core/indexers/objects.py": 409599993084996593, - "frontend/build/_app/immutable/chunks/DeED_3EO.js": 8217702195318248974, - "venv/lib/python3.13/site-packages/pydantic_settings/utils.py": 14396350455520958703, - "venv/lib/python3.13/site-packages/starlette/datastructures.py": 12097180838087229563, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_transform.py": 18159263102656491974, - "venv/lib/python3.13/site-packages/more_itertools/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/ma/core.pyi": 4829891173366130328, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_format.py": 5231304674283854194, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_head_tail.py": 13047338070883375426, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/status.py": 2214590120238144582, - "venv/lib/python3.13/site-packages/ecdsa/test_jacobi.py": 1249543655136186433, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_byteswap.py": 5090287710338893350, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi": 16042272072158438261, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/__init__.py": 1923528231486706442, - "frontend/src/components/tools/DebugTool.svelte": 17267560034225794881, - "backend/src/scripts/__init__.py": 5400372851656282343, - "backend/src/services/clean_release/demo_data_service.py": 8795028177684641724, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_np_datetime.py": 18363183516409621205, - "backend/alembic_test.db": 14995253579408279707, - "venv/lib/python3.13/site-packages/pandas/api/interchange/__init__.py": 181911269762882069, - "backend/src/plugins/translate/superset_executor.py": 454191269003159946, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py": 335201628064585213, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timezones.py": 8042029178538467146, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/pickleable.py": 7712040860601343075, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/conftest.py": 17897502508251413988, - "venv/lib/python3.13/site-packages/passlib/utils/compat/__init__.py": 17369882236606423129, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cbrt.csv": 8929553891827331780, - "venv/lib/python3.13/site-packages/starlette/middleware/cors.py": 11384893784314197864, - "venv/lib/python3.13/site-packages/urllib3/util/request.py": 9432067988603015849, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/packaging/_parser.py": 13394093358675095309, - "backend/src/schemas/dataset_review_pkg/_dtos.py": 13301885458265748263, - "venv/lib/python3.13/site-packages/numpy/_core/lib/libnpymath.a": 17555816778997028283, - "venv/lib/python3.13/site-packages/pyasn1/debug.py": 6305633719847955310, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_empty.py": 13006241382741353569, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraypad.pyi": 12178766981280242620, - "venv/lib/python3.13/site-packages/passlib/tests/utils.py": 15403198680231294263, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_formats.py": 10263085276270778330, - "venv/lib/python3.13/site-packages/packaging/_musllinux.py": 2568338440457080365, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.py": 6617116180936986481, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_value_counts.py": 5534713464063604301, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_missing.py": 3991199131191840903, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-np0-objarr.npy": 1615546603475261360, - "venv/lib/python3.13/site-packages/pygments/lexers/srcinfo.py": 16466766917840327610, - "venv/lib/python3.13/site-packages/PIL/ImageTk.py": 17063969714816564357, - "backend/src/plugins/translate/__tests__/test_sql_generator.py": 4467071942040481147, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_function_base.py": 15241999259666702619, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils_md4.py": 15270721044910698449, - "venv/lib/python3.13/site-packages/pandas/__init__.py": 793698514004377486, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/__init__.py": 3619091571279924576, - "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.py": 9654828595008925976, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_compound.f90": 15012692484070385863, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.pyi": 16976386782787920450, - "backend/src/scripts/clean_release_tui.py": 7514485779948079886, - "backend/tests/core/migration/test_dry_run_orchestrator.py": 5158126098685923841, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/__init__.py": 10834852742917144496, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine_first.py": 11425059090050867171, - "venv/lib/python3.13/site-packages/numpy/_typing/_extended_precision.py": 4201820494689784136, - "venv/lib/python3.13/site-packages/h11/_writers.py": 8585823094975171480, - "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_stmts.f90": 6123192245816905092, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tan.csv": 6233924374281534211, - "venv/lib/python3.13/site-packages/numpy/_core/records.py": 11414828908088050950, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/client_mixin.py": 17980753662107479779, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/__init__.py": 1343779312297539596, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/columns.py": 8488246354818721406, - "venv/lib/python3.13/site-packages/pillow.libs/libtiff-295fd75c.so.6.2.0": 17866578820357536656, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_indexing.py": 14552860427542050771, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/blocking.py": 3483388533957772681, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/pager.py": 18072372651585577904, - "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_s3.py": 17425535047477778484, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_constructors.py": 3440572832185940666, - "venv/lib/python3.13/site-packages/pandas/core/reshape/reshape.py": 5387892304726762127, - "venv/lib/python3.13/site-packages/authlib/common/security.py": 2291133227104016684, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/scoping.py": 18223258186431020473, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/__init__.py": 16146480198665866916, - "venv/lib/python3.13/site-packages/pandas/io/formats/console.py": 10370334487585281127, - "venv/lib/python3.13/site-packages/PIL/BlpImagePlugin.py": 7209218638257731375, - "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_records.py": 12217508428903265797, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rapidfuzz/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.pyi": 7410507092040239447, - "venv/lib/python3.13/site-packages/numpy/testing/overrides.pyi": 17408923209056015964, - "venv/lib/python3.13/site-packages/authlib/jose/errors.py": 15231759002942436632, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/meson.build.template": 9935732744553086652, - "venv/lib/python3.13/site-packages/pandas/core/ops/array_ops.py": 10061020479033421596, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/traceback.py": 2535227583726271746, - "venv/lib/python3.13/site-packages/PIL/Image.py": 13615925856053031482, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/click/shell_completion.py": 5858110451169927373, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/literal.py": 5641382379370828704, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/WHEEL": 2823347510103459191, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_quantile.py": 11966156706220948315, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_polynomial.pyi": 4266795948450921502, - "venv/lib/python3.13/site-packages/pip/_internal/locations/_distutils.py": 13559457590971821771, - "venv/lib/python3.13/site-packages/ecdsa/_compat.py": 13034064768731879065, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/pretty.py": 2448541115957861214, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/__init__.py": 15487893533770969571, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pyx": 11739218099400096619, - "venv/lib/python3.13/site-packages/pygments/lexers/savi.py": 95400050302239274, - "venv/lib/python3.13/site-packages/_pytest/cacheprovider.py": 5114545353355728201, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/pbkdf2.py": 3369246520501764730, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/numpy/testing/tests/test_utils.py": 5291132432383518704, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/__init__.py": 13749066369790839334, - "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.py": 17055498803745289016, - "venv/lib/python3.13/site-packages/pandas/core/window/numba_.py": 1444123360986509769, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE": 4123064337800029995, - "venv/lib/python3.13/site-packages/greenlet/tests/test_throw.py": 4119265751381210399, - "venv/lib/python3.13/site-packages/fastapi/_compat/may_v1.py": 9880083742927480960, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/clsregistry.py": 1589692649837935355, - "venv/lib/python3.13/site-packages/multipart/decoders.py": 11346752866822986212, - "venv/lib/python3.13/site-packages/pytest/__init__.py": 6137272437722350673, - "venv/lib/python3.13/site-packages/pygments/formatters/other.py": 13124903290103898118, - "venv/lib/python3.13/site-packages/pygments/styles/borland.py": 17570558758932580659, - "frontend/src/lib/stores/__tests__/mocks/stores.js": 14098417748659067309, - "frontend/build/_app/immutable/chunks/hPO6SNG0.js": 8027434811274919386, - "frontend/build/_app/immutable/chunks/GYN4TzDb.js": 14558718419034086057, - "backend/src/schemas/__tests__/test_settings_and_health_schemas.py": 13526616847756581689, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_aggregate.py": 15038915958422670700, - "backend/src/core/utils/superset_context_extractor/_base.py": 15730081123519745401, - "backend/src/api/routes/translate/_run_routes.py": 6617818713940695689, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_interpolate.py": 7354685880215444946, - "venv/lib/python3.13/site-packages/jeepney/bindgen.py": 7094585345946943534, - "venv/lib/python3.13/site-packages/git/types.py": 13960696584646279283, - "frontend/build/_app/immutable/chunks/DsnmJJEf.js": 17416107759626312632, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_add_prefix_suffix.py": 4815518357936092237, - "backend/src/models/auth.py": 14495240302739907540, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/scrypt.py": 15294662527620994511, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_fillna.py": 9297637486401098611, - "venv/lib/python3.13/site-packages/pygments/lexers/sgf.py": 6802546617450210439, - "venv/lib/python3.13/site-packages/pip/_internal/commands/configuration.py": 1608697090566485893, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/WHEEL": 7314207500206292683, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_api.py": 13739775047493489785, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_common.py": 9446944526144164255, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_constructors.py": 2786493777737472376, - "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.py": 11758491058846790938, - "venv/lib/python3.13/site-packages/pandas/core/internals/ops.py": 4740509262379451243, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/methods.py": 8059372834301557565, - "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/__init__.py": 4761936067832638585, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/style.py": 12654125988995362593, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_api.py": 10514120091521589282, - "venv/lib/python3.13/site-packages/iniconfig/_parse.py": 11418441008427032090, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_label_or_level_utils.py": 3423644585913150841, - "venv/lib/python3.13/site-packages/greenlet/greenlet_thread_support.hpp": 7773996382104849037, - "venv/lib/python3.13/site-packages/pandas/tests/strings/__init__.py": 5060190599837877599, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/__init__.py": 15971002342788501945, - "venv/lib/python3.13/site-packages/websockets/legacy/framing.py": 12759022769700230409, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_logical_ops.py": 4438104524064299182, - "venv/lib/python3.13/site-packages/numpy/_core/cversions.py": 9724467539503410469, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py": 11657469008721907972, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_reductions.py": 1713831062142840146, - "venv/lib/python3.13/site-packages/pygments/modeline.py": 6194365452131561478, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pygments/lexers/varnish.py": 3608370156692616959, - "venv/lib/python3.13/site-packages/gitdb/db/base.py": 3375449584942578385, - "venv/lib/python3.13/site-packages/pygments/lexers/macaulay2.py": 8022891778577314212, - "frontend/src/services/taskService.js": 11368052191249479533, - "backend/src/api/routes/translate/_preview_routes.py": 16234500901707822839, - "backend/src/models/connection.py": 7467905739729526898, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_arithmetic.py": 11620920548943067949, - "frontend/src/lib/stores/__tests__/test_sidebar.js": 113566022614194798, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_misc.py": 16393451200513733144, - "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py": 11718998093543631478, - "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.py": 16856718240972376883, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_nat.py": 7400568652984772972, - "venv/lib/python3.13/site-packages/pydantic/config.py": 1450073446492183677, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/jwt_bearer.py": 6973859422068490274, - "venv/lib/python3.13/site-packages/_pytest/config/__init__.py": 12787579251203510616, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/fields.py": 10090313990429695520, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_utils.py": 11574890848475750509, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/util.py": 9373891872370603585, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_julian_date.py": 850085757774959003, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/cli.py": 6382193953727339271, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_comment.py": 5603075924832933306, - "venv/lib/python3.13/site-packages/PIL/features.py": 3481835375516797487, - "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.pyi": 18395688193449688360, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_ufunc.py": 3326502989311831856, - "frontend/build/_app/immutable/entry/start.Q6F0ayD4.js": 9740583146990910165, - "venv/lib/python3.13/site-packages/uvicorn/server.py": 15714008348292243019, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/WHEEL": 17410883677306885019, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/licenses/LICENSE": 6653202620765194772, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_truncate.py": 6478126371906156409, - "venv/lib/python3.13/site-packages/itsdangerous/exc.py": 11181421963755661477, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/syntax.py": 18070449217682341495, - "venv/lib/python3.13/site-packages/pydantic/plugin/_loader.py": 792663962985957671, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_groupby.py": 12709199432838648700, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_pickle.py": 6232650893735715362, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/pandas/io/excel/_pyxlsb.py": 10035587889954731645, - "venv/lib/python3.13/site-packages/git/refs/log.py": 5175801243332619268, - "venv/lib/python3.13/site-packages/pygments/lexers/numbair.py": 17382646010399794697, - "venv/lib/python3.13/site-packages/git/util.py": 7424113599506269603, - "frontend/src/routes/migration/mappings/+page.svelte": 4566398049366943407, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_add_prefix_suffix.py": 8367793476246001244, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/INSTALLER": 17282701611721059870, - "backend/src/plugins/llm_analysis/scheduler.py": 14673660738877638091, - "venv/lib/python3.13/site-packages/pandas/tests/extension/list/array.py": 8065856342131357421, - "venv/lib/python3.13/site-packages/starlette/middleware/errors.py": 1337979724602864440, - "venv/lib/python3.13/site-packages/numpy/_configtool.py": 2946953194004101078, - "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py": 18317443814450756666, - "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.pyi": 8607205990821121708, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_categorical.py": 1329115818264527103, - "venv/lib/python3.13/site-packages/pygments/lexers/minecraft.py": 10133579583270214110, - "backend/src/api/routes/dashboards/_router.py": 8641007051243802224, - "venv/lib/python3.13/site-packages/jeepney/io/__init__.py": 2568357146184259175, - "venv/lib/python3.13/site-packages/pygments/formatters/groff.py": 6480830447984073574, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/keywrap.py": 10424037418528425800, - "venv/lib/python3.13/site-packages/pip/_internal/utils/hashes.py": 17310331665429541812, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_series.py": 16418555817529711079, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/multipart/exceptions.py": 8580522204565816132, - "venv/lib/python3.13/site-packages/pygments/lexers/floscript.py": 944042220740663491, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/licenses/LICENSE": 6808958893921859106, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/help.py": 3124654001287175629, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs7.py": 9623344347519683527, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py": 7732330576029516854, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/markup.py": 5413800947725916538, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multithreading.py": 12547048606098186681, - "venv/lib/python3.13/site-packages/pandas/core/computation/align.py": 13027916530558901817, - "venv/lib/python3.13/site-packages/authlib/integrations/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_subclass.py": 10949229764696473939, - "frontend/src/routes/migration/+page.svelte": 7821836225621176583, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_quantile.py": 4225791949463893856, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/ecdsa/ellipticcurve.py": 15624510849346629163, - "venv/lib/python3.13/site-packages/itsdangerous/serializer.py": 14271229091461164933, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/RECORD": 11788532857617688351, - "venv/lib/python3.13/site-packages/pygments/lexers/modula2.py": 6575895980250662093, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/METADATA": 6502714564886871819, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi": 7385072545198817771, - "frontend/src/lib/stores/__tests__/mocks/navigation.js": 10782992736745205981, - "frontend/src/lib/components/reports/__tests__/reports_page.integration.test.js": 16463062768004950066, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_unsupported.py": 9115440166074553860, - "frontend/build/_app/immutable/chunks/BwABSOzi.js": 12485550378293032886, - "venv/lib/python3.13/site-packages/pandas/core/sparse/api.py": 9094651265692218821, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/METADATA": 12295263493152296681, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/redis.py": 11313520438600819390, - "venv/lib/python3.13/site-packages/pyasn1/compat/integer.py": 7365270971777550573, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/gh19161.f90": 7691871466158049079, - "venv/lib/python3.13/site-packages/urllib3/http2/__init__.py": 62108352209996836, - "venv/lib/python3.13/site-packages/starlette/types.py": 6372850128669191435, - "venv/lib/python3.13/site-packages/httpx/_types.py": 12461152436147308761, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/METADATA": 4954909612926293404, - "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/__init__.py": 14268033439436797364, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/proxy.py": 13008557041076030198, - "venv/lib/python3.13/site-packages/pydantic/v1/annotated_types.py": 12925825196690299531, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_functions.py": 2255133375699086300, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.pyi": 15114028503984522176, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py": 1551768441102117466, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_indexing.py": 13703573544185051910, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_gen_files.py": 11149423183253618378, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/utils.py": 7707707044867513457, - "venv/lib/python3.13/site-packages/pygments/styles/emacs.py": 4540417536507245024, - "frontend/src/lib/components/layout/TaskDrawer.svelte": 6654829127438406248, - "frontend/build/_app/immutable/chunks/B-mQ6-tM.js": 4944461039645576568, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_align.py": 6050248183327934780, - "venv/lib/python3.13/site-packages/pip/_internal/cache.py": 210214424535803946, - "venv/lib/python3.13/site-packages/pyasn1/type/error.py": 1479878696626082697, - "venv/lib/python3.13/site-packages/pandas/api/executors/__init__.py": 16954693181142010028, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dropna.py": 17272771246760486947, - "backend/logs/app.log.5": 4675888931719915527, - "venv/lib/python3.13/site-packages/pillow.libs/libwebp-d8b9687f.so.7.2.0": 116215608212382352, - "venv/lib/python3.13/site-packages/pandas/core/indexes/base.py": 496956059910401121, - "venv/lib/python3.13/site-packages/httpcore/_async/http2.py": 16350817785499681224, - "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.pyi": 17639497083041129225, - "venv/lib/python3.13/site-packages/greenlet/platform/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_cisco.py": 1786475632318043669, - "venv/lib/python3.13/site-packages/jsonschema/_legacy_keywords.py": 16481666133229715822, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayterator.py": 3076316311545674897, - "venv/lib/python3.13/site-packages/websockets/http11.py": 11022534176398053082, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_version.pyi": 18093228021414910594, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/__init__.py": 11009955319062089477, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dtype.py": 5553691086633215767, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/dtype.py": 9342837412280377086, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_index.py": 10837492069089787028, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_openpyxl.py": 9542840527483618681, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_like.pyi": 1968570015765106620, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.py": 4265012785873472600, - "venv/lib/python3.13/site-packages/sqlalchemy/util/__init__.py": 15821192733097665649, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_drop_duplicates.py": 12601598844977346213, - "venv/lib/python3.13/site-packages/pygments/lexers/fortran.py": 6859638690245583968, - "frontend/src/routes/+layout.ts": 8392799000856778740, - "frontend/build/_app/immutable/chunks/DoN5Iioi.js": 15272261932303525049, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_stride_tricks.py": 1064774375151677743, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/REQUESTED": 15130871412783076140, - "frontend/build/favicon.png": 16740551781088792605, - "venv/lib/python3.13/site-packages/numpy/char/__init__.py": 4428218097234308768, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/base.py": 14734745827490220667, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_formats.py": 2743326139283991615, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/einsumfunc.py": 9944042006439149561, - "backend/src/plugins/translate/__tests__/test_dictionary.py": 17973099740734727535, - "backend/src/api/routes/git/_config_routes.py": 6093940098740660772, - "venv/lib/python3.13/site-packages/pydantic/_internal/_core_metadata.py": 5704137311404303574, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py": 5630511062945201327, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_level_values.py": 12490886203425393290, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_hist_method.py": 2109257663592928, - "venv/lib/python3.13/site-packages/pandas/tests/reductions/__init__.py": 16150188643076000414, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/padding.py": 7470483648602195268, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/foo.f": 6349567809926050379, - "venv/lib/python3.13/site-packages/apscheduler/triggers/calendarinterval.py": 12481650996882675949, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dropna.py": 3168969662216766293, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/utils.py": 617314300548081590, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_dtypes.py": 2313400030798822026, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/util.py": 2176177435997659667, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dh.pyi": 3254633032236269842, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_sh_gcc.h": 3574353221233465215, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/aead.py": 8740476942925346444, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/chararray.pyi": 9683985931409244216, - "venv/lib/python3.13/site-packages/authlib/oidc/core/models.py": 1370998944213578392, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_equals.py": 9903250467264848187, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/util.py": 2866856507760047219, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 12574929384355254457, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_setops.py": 10550603120090683350, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/utils.py": 8633356372647820801, - "venv/lib/python3.13/site-packages/passlib/hosts.py": 11171402820797380938, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/requests/auth.py": 912521029864094489, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_loc.py": 17839380711970027363, - "venv/lib/python3.13/site-packages/pandas/io/parsers/arrow_parser_wrapper.py": 11745253171984606573, - "venv/lib/python3.13/site-packages/greenlet/greenlet_compiler_compat.hpp": 465436185632243991, - "venv/lib/python3.13/site-packages/starlette/staticfiles.py": 11256974306202558529, - "venv/lib/python3.13/site-packages/pandas/_libs/writers.pyi": 10494773026133244183, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arithmetic.py": 17043958604607035425, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/generic.py": 17755579497690180620, - "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.cpython-313-x86_64-linux-gnu.so": 13917011878259810696, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/RECORD": 16558021503346615951, - "venv/lib/python3.13/site-packages/pyasn1/codec/native/decoder.py": 15336592439379775543, - "venv/lib/python3.13/site-packages/_pytest/recwarn.py": 15312759837838083596, - "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.pyi": 2744889206277418389, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_read_fwf.py": 13684564756628524738, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/METADATA": 15888766579203341778, - "venv/lib/python3.13/site-packages/itsdangerous/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/git/index/fun.py": 18338379518768926372, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming_py.py": 16133454512601432815, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_set.py": 534498057184394558, - "venv/lib/python3.13/site-packages/pygments/console.py": 3540660073442483692, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/__init__.py": 14571251344510763303, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/multiarray.py": 3224049574922696945, - "venv/lib/python3.13/site-packages/pygments/formatters/html.py": 1918541918263449443, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.py": 2569618488804767890, - "venv/lib/python3.13/site-packages/keyring/backends/Windows.py": 4103830951042463184, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/unix.py": 13808187640638677050, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90": 7589258200398878529, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_extension.py": 17245018204352082978, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_row.py": 918740441973901148, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/url.py": 1558176735588971829, - "venv/lib/python3.13/site-packages/pygments/styles/friendly.py": 13799024964803741891, - "venv/lib/python3.13/site-packages/anyio/_core/_resources.py": 8463932838064724564, - "venv/lib/python3.13/site-packages/numpy/rec/__init__.pyi": 1782063765067823837, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/rethinkdb.py": 16413299263077970925, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_mixins.py": 14293848457775268010, - "venv/lib/python3.13/site-packages/pygments/lexers/berry.py": 4540479522141160472, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL": 18203500019759199992, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_array_to_datetime.py": 3467480435568672840, - "backend/src/core/migration/dry_run_orchestrator.py": 10939333902806918334, - "venv/lib/python3.13/site-packages/passlib/handlers/misc.py": 18255658377109333797, - "venv/lib/python3.13/site-packages/greenlet/TGreenlet.hpp": 6993553444981671786, - "backend/src/services/mapping_service.py": 5976063352141712188, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_des.py": 7198931807116651263, - "frontend/src/routes/profile/__tests__/profile-settings-state.integration.test.js": 3674649277514557450, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/stride_tricks.pyi": 8257977532799807847, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/common.py": 9354442374897565498, - "venv/lib/python3.13/site-packages/pandas/core/tools/timedeltas.py": 7221393731712061943, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_formats.py": 2601044064677727807, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/__init__.py": 9884041726796476720, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/__init__.py": 9106887608906281686, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log2.csv": 13979868713429167140, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/passlib/tests/test_apache.py": 12515001998770565375, - "venv/lib/python3.13/site-packages/pygments/styles/friendly_grayscale.py": 16840106759769837649, - "frontend/src/routes/admin/settings/+page.svelte": 6706258642200752989, - "frontend/src/services/gitService.js": 7044825476968825357, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/authorization_server.py": 2913800420669417152, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-2.csv": 5423145402839576695, - "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.pyi": 12043292763005874815, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/x963kdf.py": 14995426099932276090, - "venv/lib/python3.13/site-packages/pandas/_config/localization.py": 11197430792343731762, - "venv/lib/python3.13/site-packages/ecdsa/test_ecdsa.py": 17595025859105321562, - "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/integration.py": 3085827906490477329, - "venv/lib/python3.13/site-packages/pygments/lexers/futhark.py": 10102535873433759899, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_character.py": 850554362222586199, - "venv/lib/python3.13/site-packages/pygments/lexers/carbon.py": 6753624244809529841, - "venv/lib/python3.13/site-packages/jeepney/low_level.py": 6546206679858571608, - "backend/src/services/clean_release/compliance_orchestrator.py": 10585043190405940115, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_api_info.py": 16491629653122827368, - "backend/tests/test_translate_jobs.py": 16585154497956900995, - "venv/lib/python3.13/site-packages/pygments/lexers/mosel.py": 1877592344012288438, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/protocol.py": 932296179250981622, - "run_clean_tui.sh": 14305120550349644601, - "frontend/build/_app/immutable/chunks/BoPlBqXU.js": 15621955836226522600, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_rolling.py": 11118160360072677569, - "venv/lib/python3.13/site-packages/pillow.libs/libwebpmux-7f11e5ce.so.3.1.2": 13105940093474848953, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/aiomysql.py": 12999962380333482189, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_setops.py": 17350958906336246626, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/config.py": 10707678943298497676, - "venv/lib/python3.13/site-packages/numpy/_core/_struct_ufunc_tests.cpython-313-x86_64-linux-gnu.so": 13269324887178340397, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/core/indexers/utils.py": 5861072573716897447, - "frontend/svelte.config.js": 8473636210766821560, - "venv/lib/python3.13/site-packages/numpy/version.pyi": 5075382895829889457, - "frontend/src/lib/components/translate/ScheduleConfig.svelte": 17692328825363231497, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_ewm.py": 13789842684407148830, - "venv/lib/python3.13/site-packages/attr/_typing_compat.pyi": 2823472697859409877, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/METADATA": 15970460108484113417, - "frontend/src/lib/stores/activity.js": 14023344642294459631, - "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.py": 1644743742545329282, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_chained_assignment_deprecation.py": 7694137945143129474, - "venv/lib/python3.13/site-packages/requests/sessions.py": 6936640856092320928, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py": 16550134594771291234, - "backend/alembic/versions/2a7b8c9d0e1f_add_target_languages_to_translation_jobs.py": 7201017059554918882, - "venv/lib/python3.13/site-packages/passlib/handlers/des_crypt.py": 15304795681178011265, - "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.cpp": 7360124705400993707, - "venv/lib/python3.13/site-packages/keyring/backend.py": 14185808762437650951, - "venv/lib/python3.13/site-packages/referencing/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_categorical.py": 10863578845754547294, - "venv/lib/python3.13/site-packages/pip/_internal/utils/appdirs.py": 539688259643447487, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_unicode.py": 1103608932321322989, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/METADATA": 2604612822968175379, - "backend/src/core/utils/superset_context_extractor/_recovery.py": 13298493527065532076, - "venv/lib/python3.13/site-packages/pandas/core/window/__init__.py": 15180240812077877933, - "backend/src/services/dataset_review/orchestrator_pkg/_commands.py": 15474996500928063708, - "venv/lib/python3.13/site-packages/jeepney/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_wrap.py": 17083394123203662880, - "venv/lib/python3.13/site-packages/anyio/__init__.py": 12752386096259972231, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_types.py": 7175518113263603341, - "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/mlib.ini": 16151597765994697599, - "venv/lib/python3.13/site-packages/pandas/core/methods/to_dict.py": 4653628436393663485, - "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp_impl.cpython-313-x86_64-linux-gnu.so": 11767771662042404251, - "backend/src/api/routes/translate/_correction_routes.py": 12437040676784563126, - "frontend/src/routes/storage/repos/+page.svelte": 3849368960648953479, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/android.py": 8607703347015852195, - "venv/lib/python3.13/site-packages/referencing/_core.py": 14744795336121513230, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npz": 9481735723557962988, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_resolution.py": 6076761538303762297, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_describe.py": 14493300276133670380, - "frontend/src/lib/i18n/index.ts": 2428391315568750665, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/layout.py": 11554376181083053156, - "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.pyi": 15767546107856827543, - "venv/lib/python3.13/site-packages/pandas/core/accessor.py": 1188716996113179524, - "venv/lib/python3.13/site-packages/PIL/GimpPaletteFile.py": 2040459799887417392, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/__init__.py": 12911374670789466961, - "venv/lib/python3.13/site-packages/pandas/core/interchange/utils.py": 11401792151107207834, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/provision.py": 13156893203444936309, - "venv/lib/python3.13/site-packages/pygments/lexers/bare.py": 18266858932333619451, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/auto.py": 15947958974558543657, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/serializer.py": 8708767945343850816, - "venv/lib/python3.13/site-packages/pandas/_testing/__init__.py": 10127496080404419481, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_timer.py": 8196003526646080498, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_windows.py": 9171347078900057357, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename.py": 16148017223289654683, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_size.py": 14032390806503440750, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_timedelta.py": 364160935264757405, - "venv/lib/python3.13/site-packages/numpy/typing/__init__.py": 3608665862759745048, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/gitdb/db/mem.py": 15823812455996199993, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py": 15060063324909302240, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/METADATA": 15658158392301945427, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_legacy.py": 9226692984248279094, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/RECORD": 10264342250058307301, - "venv/lib/python3.13/site-packages/cryptography/x509/oid.py": 13909847799728532602, - "venv/lib/python3.13/site-packages/numpy/lib/_iotools.py": 10522826373041592861, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/warnings_and_errors.pyi": 3151453262733619734, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/__init__.py": 4390908730288159455, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/parameters.py": 5104015814292812973, - "venv/lib/python3.13/site-packages/pandas/core/groupby/grouper.py": 18110568057754450953, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/WHEEL": 9100692507274722091, - "venv/lib/python3.13/site-packages/pandas/core/reshape/encoding.py": 12702053687535473061, - "venv/lib/python3.13/site-packages/pygments/lexers/nit.py": 18262907536841158657, - "venv/lib/python3.13/site-packages/pygments/unistring.py": 18037463865539157041, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/typst.tpl": 5055419073126793137, - "frontend/build/_app/immutable/nodes/17.D5QBfMD6.js": 12662533799528506371, - "backend/src/scripts/init_auth_db.py": 7729644769215024318, - "backend/src/core/task_manager/__tests__/test_context.py": 4211690952139950648, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/WHEEL": 9788895026336324100, - "venv/lib/python3.13/site-packages/numpy/tests/test_scripts.py": 5790352482583964348, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_cov_corr.py": 13652339565475304679, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/__init__.py": 16211693040174992594, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_util.py": 16908393806993809474, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_multi.py": 4612816468258071810, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_c_parser_only.py": 3038797485572319804, - "frontend/src/lib/ui/LanguageSwitcher.svelte": 3899734183369041614, - "backend/src/services/notifications/service.py": 7032100258837946310, - "backend/src/services/clean_release/report_builder.py": 6953311294119437848, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py": 17060377178521307324, - "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.py": 13446184230476677032, - "backend/src/plugins/translate/prompt_builder.py": 9619706108488955349, - "venv/lib/python3.13/site-packages/sqlalchemy/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/_libs/arrays.pyi": 12629806207276576877, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py": 3203921571999101793, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_timezones.py": 15157687716502574038, - "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/asn1.py": 8801155055979135092, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/adapters.py": 2224938661429637508, - "venv/lib/python3.13/site-packages/greenlet/TGreenlet.cpp": 4121171004360760212, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/__init__.py": 237070345717942041, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/RECORD": 3288669175908898843, - "venv/lib/python3.13/site-packages/sqlalchemy/event/api.py": 987321343916500337, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/expression.py": 1248029135284283479, - "venv/lib/python3.13/site-packages/numpy/core/_multiarray_umath.py": 10973016138581934035, - "venv/lib/python3.13/site-packages/numpy/conftest.py": 8971879716683736515, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.c": 9134176670140958459, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_py.py": 6518425342376455821, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.pyf": 10179237442840865225, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_index.py": 14187428934500390688, - "venv/lib/python3.13/site-packages/ecdsa/_rwlock.py": 7971972922423146201, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_limited_api.py": 15788485008764560845, - "venv/lib/python3.13/site-packages/passlib/tests/test_pwd.py": 10089138951415591061, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_mixins.py": 12351373879926460719, - "venv/lib/python3.13/site-packages/numpy/_core/strings.pyi": 1981676808027146409, - "venv/lib/python3.13/site-packages/yaml/tokens.py": 2279161238859232338, - "venv/lib/python3.13/site-packages/dateutil/tz/_common.py": 9009072600471223066, - "venv/lib/python3.13/site-packages/pygments/lexers/sieve.py": 15757825442941567629, - "backend/src/core/encryption_key.py": 10697135200011385036, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/__init__.py": 1948986914452928735, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/__init__.py": 14571251344510763303, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop.py": 17940792593116540139, - "backend/src/core/__tests__/test_superset_profile_lookup.py": 15590077829286810714, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_rowcount.py": 6682800584070485197, - "backend/src/services/reports/__tests__/test_report_service.py": 6045116534626984913, - "backend/src/models/translate.py": 1714500577896249923, - "venv/lib/python3.13/site-packages/cryptography/x509/__init__.py": 4066756344049307453, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/cmac.py": 12011944740750051829, - "venv/lib/python3.13/site-packages/numpy/_core/function_base.py": 5676611841071163854, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py": 13021437050542691118, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/response.py": 9594165377270752433, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/validation": 5398883285871046811, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py": 14954780419054588164, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pygments/lexers/_julia_builtins.py": 8846111188795315103, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/lib/array_utils.pyi": 14646405559193539455, - "backend/src/plugins/git/llm_extension.py": 9347391649685791102, - "backend/alembic/versions/aa1b2c3d4e5f_drop_deprecated_translate_columns.py": 15433357059529111004, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py": 6109201994963211857, - "venv/lib/python3.13/site-packages/pydantic/mypy.py": 10034028441785588819, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period_range.py": 9598872514187524706, - "venv/lib/python3.13/site-packages/pandas/io/parsers/python_parser.py": 7297059565749331767, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_common.py": 3559416690270529904, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraypad.py": 4907193085512342162, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_searchsorted.py": 12074107109575676897, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/_pytest/main.py": 931018496411984909, - "backend/src/api/routes/translate/_job_routes.py": 217949482917696293, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_legend.py": 13094583566764796297, - "venv/lib/python3.13/site-packages/more_itertools/more.py": 1631135582441184133, - "venv/lib/python3.13/site-packages/fastapi/openapi/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/traversals.py": 1188980668212022868, - "venv/lib/python3.13/site-packages/PIL/GimpGradientFile.py": 9777026720028891154, - "venv/lib/python3.13/site-packages/dateutil/tz/tz.py": 5258008267965780817, - "venv/lib/python3.13/site-packages/attrs/setters.py": 5775359015245600592, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_hashing.py": 11119284926039008503, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/dml.py": 17308908394336670576, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api_latest.c": 5472633890472031814, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/mod_derived_types.f90": 2556756676983488922, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/RECORD": 12575893084587070195, - "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimes.py": 10585034798097337148, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/RECORD": 8104089156015954779, - "venv/lib/python3.13/site-packages/pygments/formatters/latex.py": 7514533384468139434, - "venv/lib/python3.13/site-packages/pygments/lexers/sql.py": 15566262873151658034, - "frontend/static/favicon.png": 16740551781088792605, - "frontend/src/lib/ui/Input.svelte": 8632025378593263475, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/__init__.py": 9997757256282720103, - "venv/lib/python3.13/site-packages/fastapi/openapi/constants.py": 1111838716953068285, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/nonce.py": 18219168479887197733, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/__init__.py": 4752355175318979033, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_ratio.py": 940213488194452623, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_na_scalar.py": 13950899907067372014, - "venv/lib/python3.13/site-packages/pygments/lexers/cddl.py": 1303059539145221713, - "venv/lib/python3.13/site-packages/numpy/f2py/__version__.pyi": 9759534743796867731, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pygments/lexers/maple.py": 7402467573829618534, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_collections.py": 7772437080414835462, - "venv/lib/python3.13/site-packages/numpy/testing/tests/__init__.py": 15130871412783076140, - "frontend/src/lib/ui/Select.svelte": 11354509509920497811, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/misc/extended_precision.pyi": 6541258584648209609, - "venv/lib/python3.13/site-packages/pandas/util/_decorators.py": 14780166101442507665, - "backend/src/services/dataset_review/__init__.py": 15781509495698823404, - "venv/lib/python3.13/site-packages/click/_textwrap.py": 14586282603215052310, - "backend/alembic/env.py": 12540388445794983398, - "venv/lib/python3.13/site-packages/pygments/lexers/nix.py": 15050086330545600356, - "venv/lib/python3.13/site-packages/uvicorn/_types.py": 3339389597716220585, - "venv/lib/python3.13/site-packages/greenlet/greenlet.cpp": 3281107194043468270, - "backend/src/services/__tests__/test_llm_plugin_persistence.py": 2413185961042174999, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_util.py": 13997184496924925110, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.pyi": 2700568594438441246, - "frontend/src/lib/components/health/PolicyForm.svelte": 10697252862978287099, - "venv/lib/python3.13/site-packages/pandas/core/window/rolling.py": 6316477981414191778, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py": 13742159245132241245, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_overlaps.py": 8287716909793575347, - "venv/lib/python3.13/site-packages/ecdsa/test_malformed_sigs.py": 2606295770491528880, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_insert.py": 6970975909565157982, - "venv/lib/python3.13/site-packages/sqlalchemy/util/queue.py": 1289234691048379395, - "venv/lib/python3.13/site-packages/tzlocal/unix.py": 995019116408448073, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/scoping.py": 17216492046645992674, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE": 13479831069780783137, - "frontend/build/favicon.svg": 6451919037497541980, - "backend/src/core/auth/repository.py": 7314530014494164270, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py": 9925183787411796679, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nunique.py": 8450574751331186943, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply.py": 12349190935180702377, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/requirements.py": 11478648568700804705, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename_axis.py": 13193478481758930128, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_loop.py": 6502725909989655118, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_protocols.py": 5019018645804313814, - "backend/src/api/routes/__tests__/test_tasks_logs.py": 15585487936320145746, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_encoding.py": 6622474614967398246, - "backend/src/models/task.py": 10701108436197477339, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_numba.py": 8250377774229977284, - "venv/lib/python3.13/site-packages/numpy/random/_philox.pyi": 7503018432880404291, - "backend/src/core/utils/dataset_mapper.py": 13413818977924738147, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_join.py": 1364886077926604427, - "venv/lib/python3.13/site-packages/anyio/to_thread.py": 12767326212338028587, - "venv/lib/python3.13/site-packages/apscheduler/events.py": 10748346950729364899, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_conversion.py": 12184280074522274780, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/METADATA": 2810323639884879396, - "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js": 13047006106880121194, - "venv/lib/python3.13/site-packages/sqlalchemy/event/legacy.py": 13465911776953636949, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/util.py": 955993052529739164, - "backend/src/services/git/_gitea.py": 3585029718367932699, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/__init__.py": 13584649922093198724, - "venv/lib/python3.13/site-packages/pygments/plugin.py": 18006138132596504101, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_filters.py": 17271344092069736999, - "backend/src/services/git_service.py": 1930048176535450912, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/hooks.py": 8580161128344266243, - "venv/lib/python3.13/site-packages/numpy/_globals.pyi": 12839879732484756979, - "venv/lib/python3.13/site-packages/pandas/core/internals/construction.py": 893951849732478990, - "venv/lib/python3.13/site-packages/PIL/_webp.cpython-313-x86_64-linux-gnu.so": 11070012119632935103, - "venv/lib/python3.13/site-packages/pygments/lexers/mips.py": 2078929831513352031, - "venv/lib/python3.13/site-packages/pandas/core/ops/docstrings.py": 3721650738518391256, - "venv/lib/python3.13/site-packages/pandas/core/internals/__init__.py": 1377108808392195313, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayobject.py": 13337664596833150958, - "venv/lib/python3.13/site-packages/ecdsa/test_numbertheory.py": 2117130219401053225, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_builtin_md4.py": 16840174243259839994, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/query.py": 3867868366067025348, - "venv/lib/python3.13/site-packages/pandas/core/groupby/numba_.py": 15997665748332235683, - "venv/lib/python3.13/site-packages/pycparser/c_generator.py": 14922277346364091122, - "venv/lib/python3.13/site-packages/pygments/lexers/boa.py": 14409319610243925864, - "frontend/build/_app/immutable/nodes/15.BKKY0MZO.js": 14681615552506811594, - "backend/src/plugins/translate/service.py": 10002583160631126287, - "venv/lib/python3.13/site-packages/pip/_internal/operations/prepare.py": 13514839972066861359, - "venv/lib/python3.13/site-packages/passlib/__init__.py": 6987142392711441525, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/mean_.py": 3641008929910474457, - "venv/lib/python3.13/site-packages/httpcore/_sync/http2.py": 11167798787131917524, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/unistring.py": 18037463865539157041, - "venv/lib/python3.13/site-packages/numpy/random/bit_generator.cpython-313-x86_64-linux-gnu.so": 2574035311375507054, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/ocsp.pyi": 8666343427862784832, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/_json.py": 6718137926109998531, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertsql.py": 4633484475514351889, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/python3.npy": 4957143805477405905, - "frontend/build/_app/immutable/chunks/CzVatF6O.js": 1337487003792124464, - "venv/lib/python3.13/site-packages/numpy/__init__.pxd": 13527602429695748857, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.pyi": 11967691665766452312, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8693/__init__.py": 11065844267208884531, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hi77.f": 10617763407280454334, - "venv/lib/python3.13/site-packages/websockets/asyncio/server.py": 885415484558949506, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/METADATA": 3814104793342833872, - "venv/lib/python3.13/site-packages/pip/_internal/models/__init__.py": 13482731985065116750, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/contains.py": 3098087827310439993, - "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe_protocol.py": 12857147443873820285, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/umath/svml/LICENSE": 9584102918819171104, - "venv/lib/python3.13/site-packages/pygments/styles/gruvbox.py": 9489017112545247388, - "venv/lib/python3.13/site-packages/pygments/lexers/webassembly.py": 17811970490091406867, - "venv/lib/python3.13/site-packages/pandas/core/missing.py": 13332302791063948992, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_subclass.py": 11096337912872573314, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/urllib3/util/timeout.py": 17922362393077965047, - "venv/lib/python3.13/site-packages/passlib/handlers/mssql.py": 2800599582708170195, - "frontend/src/lib/stores/__tests__/mocks/environment.js": 16653269108049932073, - "backend/src/models/profile.py": 18027673609437659833, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.py": 183817263146423033, - "venv/lib/python3.13/site-packages/pygments/lexers/special.py": 4972294202790455038, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/themes.py": 6451091097727599550, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77comments.f": 4801926170672147659, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/RECORD": 15865206372525596795, - "venv/lib/python3.13/site-packages/pandas/api/internals.py": 5188134630996087511, - "venv/lib/python3.13/site-packages/anyio/abc/_eventloop.py": 2023190981288767991, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/METADATA": 317417773265681256, - "backend/src/api/routes/tasks.py": 4058255810929412793, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_scrypt.py": 17378030264314664250, - "venv/lib/python3.13/site-packages/ecdsa/_sha3.py": 10353612776869489378, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/challenge.py": 6388445104761482746, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/json_schema_test_suite.py": 6152158310190822762, - "venv/lib/python3.13/site-packages/requests/packages.py": 7733041271118298220, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_period.py": 15671388508498441412, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py": 12184194085993665945, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/scope.py": 4096190764836489091, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi": 14119032813912827271, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polyutils.py": 675639791972685455, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/WHEEL": 16784970174376303810, - "venv/lib/python3.13/site-packages/gitdb/utils/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oidc/core/__init__.py": 16029653645350087968, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/rec.pyi": 9140655366667421698, - "venv/lib/python3.13/site-packages/_pytest/_io/saferepr.py": 8025234471485188730, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctan.csv": 4581185622290535488, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.pyi": 10360538024015064036, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_user_array.py": 8711697474193296376, - "venv/lib/python3.13/site-packages/pandas/core/api.py": 3390809109543982410, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_core.py": 12712110303898046818, - "venv/lib/python3.13/site-packages/pandas/tests/internals/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/RECORD": 17985800801793506250, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.h": 16138871162713175599, - "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django_source.py": 15577951591316518967, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_counting.py": 5628494712451120258, - "venv/lib/python3.13/site-packages/urllib3/filepost.py": 3814725067598042920, - "frontend/src/components/PasswordPrompt.svelte": 14457083234480062595, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_repr.py": 16609300578827398344, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/exceptions.py": 11303331612406662162, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/jws.py": 7505219131537941130, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo77.f": 7353666717928699883, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_factorize.py": 17995673140216929494, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/METADATA": 3179724236887811680, - "backend/src/services/clean_release/source_isolation.py": 10193276492118254392, - "backend/tests/core/migration/test_archive_parser.py": 1909788158234142574, - "venv/lib/python3.13/site-packages/pandas/_libs/missing.cpython-313-x86_64-linux-gnu.so": 1990755879067132155, - "venv/lib/python3.13/site-packages/apscheduler/__init__.py": 8652514118183227412, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/RECORD": 2688648742701423549, - "frontend/src/lib/stores/__tests__/taskDrawer.test.js": 12307450726550947042, - "venv/lib/python3.13/site-packages/pandas/tests/extension/date/array.py": 18073740473413382768, - "venv/lib/python3.13/site-packages/numpy/linalg/__init__.pyi": 18409133021422984774, - "venv/lib/python3.13/site-packages/rpds/__init__.py": 5862197328247012275, - "venv/lib/python3.13/site-packages/pip/_internal/commands/lock.py": 14782882714168174465, - "venv/lib/python3.13/site-packages/numpy/_typing/_scalars.py": 17701938912160369190, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/RECORD": 8201759541483070364, - "venv/lib/python3.13/site-packages/pygments/lexers/prql.py": 1159462562014166646, - "venv/bin/activate.fish": 5275961582873298085, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/devicetree.py": 9820277168995231927, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/RECORD": 3617343778671172258, - "venv/lib/python3.13/site-packages/passlib/handlers/pbkdf2.py": 16686685453577026643, - "frontend/build/_app/immutable/nodes/18.DnUw7pIE.js": 5294776392406439782, - "venv/lib/python3.13/site-packages/PIL/ImageQt.py": 6607270889359138659, - "venv/lib/python3.13/site-packages/secretstorage/__init__.py": 4535964275649588672, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/METADATA": 3390257212824996989, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_describe.py": 11923715539897559626, - "venv/lib/python3.13/site-packages/pyasn1/codec/cer/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_custom_dtypes.py": 12322100282959605055, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dim2.py": 5767600151459095125, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/rsa_key.py": 7092106298733667895, - "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.pyi": 362674842164060822, - "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.hpp": 6366432579621298231, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_unix.h": 9608771997528633681, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/profiling.py": 16030397164203895281, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23879.f90": 13174951794239860144, - "venv/lib/python3.13/site-packages/pip/_internal/network/cache.py": 10606522397693824641, - "venv/lib/python3.13/site-packages/git/exc.py": 14278238357119816365, - "venv/lib/python3.13/site-packages/greenlet/TThreadState.hpp": 15599148954148720463, - "venv/lib/python3.13/site-packages/pip/_internal/utils/datetime.py": 6816292030061301011, - "venv/lib/python3.13/site-packages/pandas/io/excel/_odswriter.py": 17220892094904423676, - "venv/lib/python3.13/site-packages/dateutil/parser/__init__.py": 11377431154251655738, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_defmatrix.py": 2982572083770917411, - "venv/lib/python3.13/site-packages/charset_normalizer/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/webidl.py": 8097260111959278117, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_blocking.py": 6312017389529916004, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_fields.py": 16222559851336781264, - "venv/lib/python3.13/site-packages/pygments/lexers/ptx.py": 4855144232064607995, - "venv/lib/python3.13/site-packages/numpy/_typing/_nbit.py": 15498810174380798405, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/linalg.pyi": 269481015135399748, - "venv/lib/python3.13/site-packages/authlib/jose/__init__.py": 317573635113539563, - "backend/_convert_defs.py": 208535643309836247, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_m68k_gcc.h": 6580173800099450404, - "venv/lib/python3.13/site-packages/pygments/styles/pastie.py": 15784474766258831882, - "backend/src/api/routes/dataset_review.py": 3689614039699023362, - "backend/src/services/__tests__/test_llm_prompt_templates.py": 17912427710321938757, - "backend/src/services/health_service.py": 7295235621662158359, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/WHEEL": 5179340427739743287, - "venv/lib/python3.13/site-packages/pygments/lexers/scripting.py": 4167168074165473790, - "backend/tests/test_translate_corrections.py": 1228642484417924184, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_concat.py": 3788461606817482165, - "venv/lib/python3.13/site-packages/git/objects/base.py": 11498407361286170399, - "venv/lib/python3.13/site-packages/pandas/io/excel/_openpyxl.py": 6676451564208867775, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937.py": 7299586490442539723, - "venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py": 450504439085405418, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c": 17333399794446198412, + "backend/src/plugins/git_plugin.py": 13646960709476277927, "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE.PSF": 13208428090829817329, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_replace.py": 18039668944453454689, - "venv/lib/python3.13/site-packages/numpy/_distributor_init.pyi": 1308368231471324581, - "venv/lib/python3.13/site-packages/numpy/lib/_iotools.pyi": 12873243737872788349, - "venv/lib/python3.13/site-packages/PIL/_imagingft.cpython-313-x86_64-linux-gnu.so": 11054435406031360190, - "venv/lib/python3.13/site-packages/passlib/pwd.py": 14727089240749133060, - "frontend/src/components/TaskRunner.svelte": 18014857434320509117, - "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.py": 1809069379864985298, - "venv/lib/python3.13/site-packages/passlib/apps.py": 4633269405597895766, - "venv/lib/python3.13/site-packages/fastapi/exception_handlers.py": 9907950997607808699, - "venv/lib/python3.13/site-packages/pygments/lexers/verification.py": 16645885125604874183, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/__init__.py": 11157967642611612522, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23533.f": 12558972205518833024, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.APACHE": 8359973023624924624, - "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.pyi": 6286629088867436848, - "venv/lib/python3.13/site-packages/numpy/random/_common.pyi": 5094115692952609118, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_monotonic.py": 11817523305776951162, - "venv/lib/python3.13/site-packages/pandas/core/construction.py": 10646956189518908985, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_frequencies.py": 14039315221824123200, - "venv/lib/python3.13/site-packages/PIL/WebPImagePlugin.py": 5378349556563611996, - "venv/lib/python3.13/site-packages/pandas/tests/util/conftest.py": 11143177766735481660, - "venv/lib/python3.13/site-packages/pygments/lexers/hexdump.py": 10665271113239513484, - "venv/lib/python3.13/site-packages/packaging/requirements.py": 2896326326372719999, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numeric.pyi": 17005748821769179511, - "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.py": 5570375156851166818, - "venv/lib/python3.13/site-packages/jeepney/io/trio.py": 11859775321483330469, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_reindex.py": 15680924058960901067, - "venv/lib/python3.13/site-packages/ecdsa/test_ecdh.py": 3338834550392548171, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.pyi": 16164841755339571545, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_case_justify.py": 8204537837649550062, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/provision.py": 9472538482404392519, - "venv/lib/python3.13/site-packages/PIL/_util.py": 17116054813914373606, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/WHEEL": 18203500019759199992, - "venv/lib/python3.13/site-packages/referencing/_attrs.pyi": 10030665705880994025, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufuncs.pyi": 14995898537484305307, - "venv/lib/python3.13/site-packages/pygments/lexers/make.py": 5345282370426217663, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_dispatcher.py": 2063037275809439611, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_cut.py": 7800833253841657380, - "venv/lib/python3.13/site-packages/anyio/streams/file.py": 15232345967545920863, - "venv/lib/python3.13/site-packages/anyio/streams/memory.py": 11762722143503428823, - "venv/lib/python3.13/site-packages/pandas/_libs/testing.pyi": 16691791330088523786, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/provision.py": 8707620698060818654, - "frontend/build/_app/immutable/chunks/DmuwojIM.js": 5778224784187071361, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/code.py": 9700811903057180696, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/WHEEL": 18203500019759199992, - "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.py": 17277594393032382485, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_arithmetic.py": 16829312896275893002, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_equals.py": 6429101196875333574, - "venv/lib/python3.13/site-packages/PIL/XpmImagePlugin.py": 6644626557482464312, - "venv/lib/python3.13/site-packages/PIL/PSDraw.py": 1927169421822446045, - "venv/lib/python3.13/site-packages/pygments/lexers/graph.py": 10445044822847205061, - "venv/lib/python3.13/site-packages/PIL/_imagingft.pyi": 1385456134120145584, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_get_numeric_data.py": 5896318696417090902, - "venv/lib/python3.13/site-packages/pygments/lexers/monte.py": 15158829991357994573, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/AUTHORS": 10073225185964928425, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py": 11917862648584782420, - "venv/lib/python3.13/site-packages/pygments/lexers/dax.py": 10868881057512002309, - "venv/lib/python3.13/site-packages/PIL/Jpeg2KImagePlugin.py": 16905402423248975204, - "venv/lib/python3.13/site-packages/pandas/tests/window/__init__.py": 15130871412783076140, - "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py": 10703957986116846695, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/pyinstaller-smoke.py": 13570990531070630950, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_common.py": 10797243274520625867, - "backend/src/api/routes/dashboards/_action_routes.py": 11814954258886577640, - "venv/lib/python3.13/site-packages/passlib/tests/_test_bad_register.py": 5446848310854472376, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pct_change.py": 18056235438452125034, - "venv/lib/python3.13/site-packages/PIL/IcnsImagePlugin.py": 1870235606309784691, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/inout.f90": 1521999519583206538, - "venv/lib/python3.13/site-packages/pandas/errors/__init__.py": 13444371880185740908, - "backend/src/core/utils/matching.py": 12567367681677699487, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/histograms.pyi": 2281259341486424755, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_na_indexing.py": 8712547126522523899, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/conftest.py": 5864037534050136837, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_first_valid_index.py": 15763400665851423183, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_fiscal.py": 14652343698825214870, - "backend/src/services/clean_release/__tests__/test_source_isolation.py": 17714198919084455758, - "venv/lib/python3.13/site-packages/numpy/core/__init__.py": 10397294636564169377, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_item_selection.py": 1027989019709029615, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/assertion_session.py": 18109848200936213195, - "venv/lib/python3.13/site-packages/numpy/ma/core.py": 338350463523585845, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/exceptions.pyi": 9294219058621261971, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_replace.py": 4953527309376861139, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_shares_memory.py": 2911029123657970431, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/impl.py": 4687705587963980337, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame.py": 16757570551941104401, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/default_styles.py": 4469204219504675080, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/requests.py": 14847842002253212098, - "venv/lib/python3.13/site-packages/dateutil/easter.py": 14631233890122048084, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/refresh_token.py": 17927903035152872556, - "venv/lib/python3.13/site-packages/pandas/_libs/interval.cpython-313-x86_64-linux-gnu.so": 8964760352224019544, - "venv/lib/python3.13/site-packages/PIL/_imagingmorph.cpython-313-x86_64-linux-gnu.so": 11171810899664471137, - "venv/lib/python3.13/site-packages/pycparser/_ast_gen.py": 5464347629908906775, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py": 17765129030290410902, - "venv/lib/python3.13/site-packages/pygments/lexers/bibtex.py": 14652372822443154388, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reshaping.py": 18085648077811502689, - "venv/lib/python3.13/site-packages/pygments/lexers/supercollider.py": 15306357945850389082, - "backend/src/core/config_models.py": 4264626520931642357, - "backend/tests/test_translate_history.py": 14504549443376034614, - "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.pyi": 14069586330277925998, - "venv/lib/python3.13/site-packages/itsdangerous/signer.py": 3694661094753475840, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_subclass.py": 16342917051908786497, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_indexing.py": 6539365174681852769, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_arithmetic.py": 4858019700859362660, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_format.py": 16150524965028369071, - "frontend/src/lib/components/translate/TranslationRunResult.svelte": 10499421223016291488, - "venv/lib/python3.13/site-packages/git/refs/tag.py": 9979537867501757110, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/signals.py": 10390443713463805090, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/__init__.py": 13331622784664204066, - "venv/lib/python3.13/site-packages/pandas/plotting/__init__.py": 13230487326686617492, - "venv/lib/python3.13/site-packages/pygments/styles/inkpot.py": 10516710471319090846, - "venv/lib/python3.13/site-packages/pygments/lexers/int_fiction.py": 8806961041808606055, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/registration.py": 14639972512303032712, - "venv/lib/python3.13/site-packages/PIL/_imagingcms.cpython-313-x86_64-linux-gnu.so": 15198265718775753589, - "venv/lib/python3.13/site-packages/rapidfuzz/process.py": 3870006735299537016, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_compat.py": 4677414058216516380, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py": 6510864297510700136, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_overrides.py": 92685611709898742, - "venv/lib/python3.13/site-packages/pandas/tests/api/test_types.py": 8889829773923216769, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ma.pyi": 6467636381637286867, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hashes.py": 8545380307201732345, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/multiarray.pyi": 6368268452184570195, - "venv/lib/python3.13/site-packages/anyio/_core/_tasks.py": 14104828341875599130, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/WHEEL": 3749476255729626245, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/introspection.py": 5447179017910128771, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath.py": 13010939919217862164, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64_np126.pkl.gz": 1630203486813146863, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.pyi": 2700568594438441246, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authorization_server.py": 13696038495666024258, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs12.py": 5946672289916692272, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils.py": 10362977406431569031, - "venv/lib/python3.13/site-packages/attrs/exceptions.py": 2871247302586505373, - "venv/lib/python3.13/site-packages/httpcore/_sync/connection.py": 1539337414493486168, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/logging.py": 2426447364230673649, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/utils.h": 3920393832993063886, - "backend/src/services/clean_release/repositories/candidate_repository.py": 14784344453820731941, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/dtype_api.h": 17040779421165903019, - "frontend/src/lib/components/translate/__tests__/test_correction_cell.svelte.js": 2314051239468628554, - "venv/lib/python3.13/site-packages/PIL/ImageCms.py": 5610897002957128546, - "venv/lib/python3.13/site-packages/pandas/_libs/hashing.pyi": 5241441378073929968, - "venv/lib/python3.13/site-packages/numpy/tests/test__all__.py": 10689265249302359388, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_smoke.py": 7476432112986643847, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcom_err-2abe824b.so.2.1": 12870528025628871295, - "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.pyi": 6546751036506752550, - "venv/lib/python3.13/site-packages/pygments/styles/coffee.py": 17584414468786822371, - "venv/lib/python3.13/site-packages/pydantic/typing.py": 6661252174518316760, - "venv/lib/python3.13/site-packages/numpy/random/_pickle.py": 7774620933480975313, - "venv/lib/python3.13/site-packages/pandas/_libs/properties.cpython-313-x86_64-linux-gnu.so": 7092136701092358552, - "venv/lib/python3.13/site-packages/numpy.libs/libgfortran-040039e1-0352e75f.so.5.0.0": 13946067868274850557, - "venv/lib/python3.13/site-packages/numpy/tests/test_ctypeslib.py": 9592016432737074765, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_methods.py": 12694335778731418517, - "backend/src/plugins/translate/events.py": 14264965160929343917, - "frontend/src/lib/stores/__tests__/mocks/env_public.js": 2540122820969872185, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/bitgen.h": 12421210767114721499, - "venv/lib/python3.13/site-packages/pip/_internal/models/target_python.py": 422323125651848065, - "venv/lib/python3.13/site-packages/pydantic/_internal/_import_utils.py": 4029157986210295967, - "venv/lib/python3.13/site-packages/keyring/core.py": 2764269590123131634, - "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.pyi": 84606125837677197, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/dtype.pyi": 12178008080078679769, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_direct.py": 12680113511641199070, - "backend/src/api/routes/assistant/_routes.py": 6252816315418755346, - "venv/lib/python3.13/site-packages/pandas/util/_exceptions.py": 14426860079366867683, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_sparse_accessor.py": 6774374030220202353, - "venv/lib/python3.13/site-packages/pandas/util/__init__.py": 4070574585004336241, - "venv/lib/python3.13/site-packages/PIL/WmfImagePlugin.py": 15685062825106444247, - "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oidc/registration/claims.py": 4161520263207458028, - "venv/lib/python3.13/site-packages/apscheduler/executors/debug.py": 14072221218785494847, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarbuffer.py": 16859789411010045795, - "venv/lib/python3.13/site-packages/PIL/JpegImagePlugin.py": 10621639599916796689, - "venv/lib/python3.13/site-packages/numpy/_core/printoptions.py": 3897105663572772838, - "venv/lib/python3.13/site-packages/keyring/compat/py312.py": 17581691618899404491, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_na_values.py": 13527837495439739279, - "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/test_jsonschema_specifications.py": 12693210357718751894, - "venv/lib/python3.13/site-packages/numpy/core/function_base.py": 12696640832638144604, - "venv/lib/python3.13/site-packages/PIL/_imaging.cpython-313-x86_64-linux-gnu.so": 1425926264271197601, - "venv/lib/python3.13/site-packages/pygments/lexers/teal.py": 10004159499175716139, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resample_api.py": 3278209206670699265, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarprint.py": 9482100952562512933, - "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.cpython-313-x86_64-linux-gnu.so": 16327106317503267067, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_py.py": 13809503317864539103, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py": 8412594868333244844, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/privatemod.f90": 8999550841368314739, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_datetime.py": 4179112383995346046, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_normalize.py": 11373923576259976755, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_indexing.py": 9862204223281834360, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_week.py": 5756727157937218128, - "venv/lib/python3.13/site-packages/pygments/styles/bw.py": 869630132791421268, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimes.py": 8863748366184419339, - "venv/lib/python3.13/site-packages/cffi/_shimmed_dist_utils.py": 4824806157907607487, - "frontend/src/components/git/BranchSelector.svelte": 6225347983501401973, - "venv/lib/python3.13/site-packages/PIL/_imagingcms.pyi": 12476347428820346383, - "venv/lib/python3.13/site-packages/referencing/tests/test_retrieval.py": 10782087416909177703, - "frontend/build/_app/immutable/nodes/30.D3JMnFof.js": 16119902291510484631, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/array.py": 3517673819082574078, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_arrow_interface.py": 9757086292358759236, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/RECORD": 6518517239543344179, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/validator.py": 11486125578281617670, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_exceptions.py": 12145393002202744314, - "venv/lib/python3.13/site-packages/pygments/lexers/dns.py": 17149871769553794654, - "venv/lib/python3.13/site-packages/greenlet/greenlet_refs.hpp": 8525862687669040510, - "backend/tests/core/test_defensive_guards.py": 1048600814279591148, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape_base.pyi": 5046405609030418900, - "venv/lib/python3.13/site-packages/pygments/lexers/jsx.py": 13855228003748707341, - "venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py": 13594639612360346306, - "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.pyi": 5892973170789971535, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_constructors.py": 979131987837682435, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_keywords.py": 11487954623214475915, - "venv/lib/python3.13/site-packages/pygments/lexer.py": 3658450949169647228, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py": 6152202474302791232, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/RECORD": 1164466882742493155, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py": 14454690938977069049, - "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_generation_shared.py": 23345689300073549, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_replace.py": 8165088653532047247, - "venv/lib/python3.13/site-packages/pyasn1/type/opentype.py": 340146737126677655, - "venv/lib/python3.13/site-packages/anyio/_core/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_hashtable.py": 7762405492052106717, - "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.pyi": 17825266810219495672, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/flatiter.py": 3066469329738423021, - "venv/lib/python3.13/site-packages/pip/_internal/index/sources.py": 11609272011565024668, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/_pytest/compat.py": 10046464101087327473, - "venv/lib/python3.13/site-packages/passlib/tests/tox_support.py": 9200774792427691602, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py": 1129954411080617001, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_set_value.py": 4166411740676564820, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_parse_dates.py": 5344929031479212951, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/string.f": 17328443345813567449, - "venv/lib/python3.13/site-packages/pip/_internal/index/collector.py": 7454652626329467900, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/licenses/LICENSE": 1641332741515411734, - "venv/lib/python3.13/site-packages/numpy/dtypes.pyi": 6713822658171717369, - "venv/lib/python3.13/site-packages/sqlalchemy/util/concurrency.py": 1325673503812009695, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_base.py": 8678387511873412922, - "venv/lib/python3.13/site-packages/pip/_internal/cli/progress_bars.py": 7584699216537478969, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_elffile.py": 5728213398707817049, - "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp.py": 16797760903996739079, - "venv/lib/python3.13/site-packages/apscheduler/executors/gevent.py": 17122637996105344344, - "venv/lib/python3.13/site-packages/numpy/random/__init__.py": 11732100564412485633, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_setitem.py": 2059440410065814114, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/bar.py": 12859947592168238832, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.py": 11225173272050906977, - "venv/lib/python3.13/site-packages/pygments/lexers/qvt.py": 1895735583818227972, - "backend/src/api/routes/__tests__/conftest.py": 8465980330386544642, - "venv/lib/python3.13/site-packages/pandas/_libs/pandas_parser.cpython-313-x86_64-linux-gnu.so": 18056339336364727359, - "venv/lib/python3.13/site-packages/numpy/ma/__init__.py": 214772578978182427, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.pyi": 1826323011169529229, - "venv/lib/python3.13/site-packages/urllib3/_version.py": 839747992523118887, - "venv/lib/python3.13/site-packages/pygments/lexers/mojo.py": 8616283481592061285, - "backend/src/schemas/dataset_review.py": 15720223123457526104, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/resource_protector.py": 17080006010679257864, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/base.py": 5924592154431932923, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/twodim_base.pyi": 10156504423258357540, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/licenses/LICENSE": 9235234428907927646, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py": 7301817934902711510, - "venv/lib/python3.13/site-packages/pygments/styles/murphy.py": 8177992406082781615, - "venv/lib/python3.13/site-packages/pygments/styles/lightbulb.py": 12433916113773598760, - "venv/lib/python3.13/site-packages/pygments/lexers/whiley.py": 218468511185731869, - "venv/lib/python3.13/site-packages/pygments/lexers/_openedge_builtins.py": 4979988747924637739, - "venv/lib/python3.13/site-packages/pandas/_libs/json.cpython-313-x86_64-linux-gnu.so": 2436285663630821963, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/METADATA": 14036738037171597049, - "venv/lib/python3.13/site-packages/pygments/styles/default.py": 13992125975366109635, - "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft_umath.cpython-313-x86_64-linux-gnu.so": 16443444849116065961, - "venv/lib/python3.13/site-packages/jeepney/bus.py": 5163075760432139100, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.pyi": 15498895254516573071, - "venv/lib/python3.13/site-packages/certifi/core.py": 7472627735517449185, - "venv/lib/python3.13/site-packages/ecdsa/der.py": 9374566189581725372, - "venv/lib/python3.13/site-packages/pydantic/error_wrappers.py": 11273827025986230629, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/licenses/LICENSE": 5757922580822401219, - "venv/lib/python3.13/site-packages/_pytest/unraisableexception.py": 4597178803320514675, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/RECORD": 11976403473353722969, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fft.pyi": 14454280827038327842, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arrow_compat.py": 12986187285988432338, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/WHEEL": 5990830714395966792, - "backend/src/plugins/translate/__tests__/test_scheduler.py": 4988970087981965515, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_function_base.pyi": 2792940598392356268, - "venv/lib/python3.13/site-packages/pygments/styles/rrt.py": 9490973359165080360, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py": 5450931608244830335, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__init__.py": 8811477458865810434, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/pyproject.py": 12684373765476932108, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh27697.f90": 4990763135487995517, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_features.py": 955759654189150497, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/live_render.py": 14619617939683800204, - "venv/lib/python3.13/site-packages/pygments/lexers/_cl_builtins.py": 16540712925368297957, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_bin_groupby.py": 14831211659124136806, - "venv/lib/python3.13/site-packages/numpy/_core/numeric.pyi": 17785360553418386488, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cos.csv": 12896264205153363031, - "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/test_uuid.py": 13955678427426850807, - "venv/lib/python3.13/site-packages/pygments/lexers/ride.py": 14096748896451437594, - "venv/lib/python3.13/site-packages/pydantic/v1/decorator.py": 6124748023704700724, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/provision.py": 7362363963464621570, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py": 5951912505933673835, - "venv/lib/python3.13/site-packages/_pytest/debugging.py": 16135285365306979060, - "venv/lib/python3.13/site-packages/_pytest/runner.py": 5772610501857496741, - "venv/lib/python3.13/site-packages/pygments/lexers/jsonnet.py": 1314044008440065525, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_conversion.py": 9159276697339089788, - "backend/src/models/dataset_review_pkg/_clarification_models.py": 1561486208194983219, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval.py": 1228451182688768814, - "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.pyi": 18290764713161282980, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/registry.py": 13306388361671753373, - "venv/lib/python3.13/site-packages/authlib/oidc/discovery/__init__.py": 308178910015974550, - "venv/lib/python3.13/site-packages/h11/_version.py": 9334144031400715479, - "venv/lib/python3.13/site-packages/referencing/tests/test_referencing_suite.py": 8875120876162738440, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.pyi": 51728851886466199, - "venv/lib/python3.13/site-packages/cryptography/py.typed": 15130871412783076140, - "dist/docker/backend.v1.0.0-rc2-docker.tar": 9693793465773386616, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/__init__.py": 18036348770966744795, - "venv/lib/python3.13/site-packages/greenlet/tests/test_generator_nested.py": 12822240347707346718, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_backend.py": 17793139799410518847, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/lambdas.py": 13377298868598623228, - "venv/lib/python3.13/site-packages/fastapi/openapi/utils.py": 9058442333718187759, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_random.py": 4179188367630060654, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpcre-9513aab5.so.1.2.0": 12298861477332583668, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_astype.py": 8505605634958454768, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_reductions.py": 3227705565578016389, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_as_unit.py": 8492383355919959393, - "venv/lib/python3.13/site-packages/dateutil/tz/__init__.py": 9617254832690762236, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_math.h": 16701045088211015518, - "venv/lib/python3.13/site-packages/numpy/lib/mixins.py": 7404729635424818497, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/conftest.py": 2643915042664055969, - "venv/lib/python3.13/site-packages/pygments/lexers/wgsl.py": 9718384325049172855, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nested_sequence.pyi": 11245460325970663922, - "venv/lib/python3.13/site-packages/requests/help.py": 1885413874621672882, - "venv/lib/python3.13/site-packages/greenlet/tests/test_cpp.py": 10982107081267457286, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/__init__.py": 3673322722277252961, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/requirements.py": 2362917888844255805, - "frontend/src/types/backup.ts": 1741234034896689348, - "frontend/src/routes/tools/storage/+page.svelte": 2651964385884112547, - "venv/lib/python3.13/site-packages/numpy/_typing/_array_like.py": 2161873439361281488, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/licenses/LICENSE": 14230156892574098522, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/__init__.py": 10357835306532281617, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_spinners.py": 15873416546130487469, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_tokenizer.py": 7994281991455916540, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/index_tricks.pyi": 11463747060334597941, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_round.py": 8336918400205461173, - "venv/lib/python3.13/site-packages/websockets/__init__.py": 4916307256304054040, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/__init__.py": 18069619555394133945, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2py2e.py": 8571347023940385644, - "venv/lib/python3.13/site-packages/pip/_internal/models/search_scope.py": 9282393652860589543, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_equals.py": 18261810467289276844, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_insert.py": 11635164392966270621, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_is_full.py": 9577318758326279694, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_select_dtypes.py": 3530442956913622572, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/io.py": 11072022722163954973, - "venv/lib/python3.13/site-packages/passlib/utils/compat/_ordered_dict.py": 14248790502300368024, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_clipboard.py": 12674078499544938252, - "venv/lib/python3.13/site-packages/pygments/styles/onedark.py": 17580335614588531275, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_searchsorted.py": 11208047646304753715, - "venv/lib/python3.13/site-packages/rapidfuzz/process.pyi": 6959881293311340830, - "venv/lib/python3.13/site-packages/keyring/backends/kwallet.py": 3071915635647280308, - "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.pyi": 2667658052734929089, - "venv/lib/python3.13/site-packages/fastapi/responses.py": 6092572095549573593, - "venv/lib/python3.13/site-packages/pydantic/env_settings.py": 18250976255197243083, - "venv/lib/python3.13/site-packages/numpy/random/_common.cpython-313-x86_64-linux-gnu.so": 12834724130890991478, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/scipy_sparse.py": 2091608752719976909, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py": 11422235699126032386, - "venv/lib/python3.13/site-packages/pandas/tests/internals/test_api.py": 11217428888457119601, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/resources.py": 11533706401259147051, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/ssh.py": 16262079536832961001, - "venv/lib/python3.13/site-packages/pygments/lexers/gleam.py": 5161116162065925222, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_parquet.py": 11050150015772440923, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sample.py": 16877779557071324115, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_longtable.tpl": 12723621983797415444, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/poly1305.py": 10854598965202354516, - "venv/lib/python3.13/site-packages/keyring/http.py": 1408024724330078009, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/METADATA": 11759004913807981650, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py": 10379929935087904974, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/RECORD": 3283324710045856319, - "venv/lib/python3.13/site-packages/pygments/lexers/css.py": 517586374264113606, - "backend/src/api/routes/translate/_metrics_routes.py": 13468336445030118059, - "venv/lib/python3.13/site-packages/greenlet/greenlet_exceptions.hpp": 8819301638282185623, - "backend/src/models/dataset_review.py": 5814500849969423218, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_string.py": 15542851215119662814, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/METADATA": 9458019064618635438, - "venv/lib/python3.13/site-packages/pandas/tests/api/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libldap-2974a1ba.so.2.0.200": 13590569080612602445, - "venv/lib/python3.13/site-packages/fastapi/openapi/models.py": 1680354178147364813, - "venv/lib/python3.13/site-packages/numpy/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/smmap/test/test_util.py": 5634593621291240094, - "venv/lib/python3.13/site-packages/pandas/tests/config/test_localization.py": 9524481848419616725, - "frontend/src/lib/components/reports/__tests__/report_detail.ux.test.js": 11878832350632571462, - "venv/lib/python3.13/site-packages/pygments/lexers/thingsdb.py": 6073447481627395832, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.py": 12355826783958402233, - "venv/lib/python3.13/site-packages/sqlalchemy/util/topological.py": 11586868018611359415, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/RECORD": 13494296690334916257, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_laguerre.py": 7475592201750602042, - "venv/lib/python3.13/site-packages/pygments/lexers/ada.py": 17032408976634308999, - "venv/lib/python3.13/site-packages/psycopg2/extensions.py": 14197445323992851566, - "venv/lib/python3.13/site-packages/pygments/lexers/ezhil.py": 10294520768762308887, - "venv/lib/python3.13/site-packages/anyio/_core/_sockets.py": 10525237827959090532, - "venv/lib/python3.13/site-packages/fastapi/exceptions.py": 12665775976182239091, - "venv/lib/python3.13/site-packages/pygments/lexers/ambient.py": 18106929781201758080, - "backend/src/plugins/translate/__tests__/test_orthogonal_fixes.py": 17679703384686060315, - "backend/src/api/routes/__tests__/test_migration_routes.py": 4357515184436771537, - "backend/alembic.ini": 8897071018956157084, - "backend/alembic/versions/ed310b33f02c_multi_language_translation_tables.py": 1424919631205944664, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/__init__.py": 635200170992032966, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resampler_grouper.py": 8388490036390951341, - "venv/lib/python3.13/site-packages/jsonschema/__init__.py": 6925762200139836824, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel.py": 7728646523045538484, - "venv/lib/python3.13/site-packages/pandas/tests/internals/test_internals.py": 2049484047553296860, - "frontend/build/_app/immutable/chunks/B59ks8Wc.js": 12257988639088999545, - "venv/lib/python3.13/site-packages/pip/_internal/cli/__init__.py": 11914508471759255927, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/errors.py": 6538052228921426956, - "venv/lib/python3.13/site-packages/httpcore/_sync/__init__.py": 7676937747094511752, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_read.py": 8825002468745406134, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_value_counts.py": 12340872812120847127, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_floats.py": 3064615530495290792, - "venv/lib/python3.13/site-packages/pygments/formatters/pangomarkup.py": 17135620955205945255, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_categorical_equal.py": 7942323667866280872, - "venv/lib/python3.13/site-packages/PIL/_imagingmath.pyi": 18222325750818585549, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_generic.py": 1098361087956787007, - "venv/lib/python3.13/site-packages/authlib/__init__.py": 8957417237987377675, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_format.py": 1947585389944838076, - "backend/src/schemas/__init__.py": 8700952869915187087, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/eoo.py": 5126566925877730303, - "venv/lib/python3.13/site-packages/authlib/oidc/core/errors.py": 18154419146257747681, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/dotenv.py": 5872848911499749500, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/hybrid.py": 5793056975404534989, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/METADATA": 6596896092754256988, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/licenses/LICENSE": 6965457853328313914, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/_libs/groupby.cpython-313-x86_64-linux-gnu.so": 14931394220112658268, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/operators.py": 2468046993343796128, - "venv/lib/python3.13/site-packages/packaging/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pillow.libs/libfreetype-ee1c40c4.so.6.20.4": 12301510769231120312, - "backend/tests/services/clean_release/test_policy_resolution_service.py": 3471271299977247598, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_api_info.pyi": 7887489694871300469, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/resource_protector.py": 4823504113176750140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/fromnumeric.py": 3792079757208879362, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunc_config.pyi": 11754104063443791746, - "venv/lib/python3.13/site-packages/PIL/BdfFontFile.py": 10811640174253687845, - "backend/src/services/clean_release/repositories/report_repository.py": 17952038790457725175, - "venv/lib/python3.13/site-packages/jeepney/tests/secrets_introspect.xml": 10717038359151125278, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_update.py": 12293424926920594944, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/names.py": 705261158040926128, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numba.py": 18148155441554555618, - "venv/lib/python3.13/site-packages/pip/_vendor/typing_extensions.py": 10158933506062403688, - "venv/lib/python3.13/site-packages/pygments/filters/__init__.py": 2992007913496948339, - "venv/lib/python3.13/site-packages/pygments/lexers/promql.py": 8929925488296216854, - "venv/lib/python3.13/site-packages/httpcore/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/utils/handlers.py": 10182859005870689026, - "venv/lib/python3.13/site-packages/numpy/exceptions.pyi": 13942486291715303235, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/_cryptography_key.py": 388489172928969873, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/_orm_constructors.py": 4276726508722638048, - "venv/lib/python3.13/site-packages/pluggy/_warnings.py": 2664977718908223842, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.py": 17538029557373507617, - "venv/lib/python3.13/site-packages/urllib3/__init__.py": 1854778274753799815, - "venv/lib/python3.13/site-packages/pygments/lexers/tlb.py": 1169242051301155885, - "backend/src/api/routes/environments.py": 6892252055515346170, - "venv/lib/python3.13/site-packages/pygments/lexers/urbi.py": 1611762607688882372, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_fillna.py": 7349777469648334282, - "frontend/src/lib/ui/Button.svelte": 921807193049478213, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/multiarray.pyi": 17232959648651278910, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/common.py": 11384497819737275399, - "venv/lib/python3.13/site-packages/gitdb/const.py": 14122142034887286559, - "venv/lib/python3.13/site-packages/passlib/ext/django/utils.py": 8118896525895185626, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_table.tpl": 8706980975714184465, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/events.py": 15099787454570172797, - "venv/lib/python3.13/site-packages/h11/_events.py": 13800479928851716538, - "venv/lib/python3.13/site-packages/jeepney/io/threading.py": 525625939662612746, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ma.pyi": 8186102816080813799, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate_regression.py": 17425202198134409689, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_extras.py": 16160090335191788104, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/accumulate.py": 5780225603819849877, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_map.py": 6556132946198785761, - "venv/lib/python3.13/site-packages/rpds/rpds.cpython-313-x86_64-linux-gnu.so": 561326734302492859, - "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.pyi": 2042169688753984918, - "venv/lib/python3.13/site-packages/httpcore/_backends/base.py": 5491727479625093183, - "venv/lib/python3.13/site-packages/pygments/styles/algol_nu.py": 6229131016121450375, - "frontend/src/lib/components/assistant/AssistantChatPanel.svelte": 15111082609123137207, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_mips_unix.h": 17373389962405490482, - "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.py": 1262543237476191714, - "backend/src/api/routes/__tests__/test_reports_detail_api.py": 6960149894653710507, - "venv/lib/python3.13/site-packages/anyio/from_thread.py": 7973678341601453474, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py": 9331389900588353667, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed25519.py": 2285778903282667561, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/constrain.py": 6513876973118703677, - "frontend/build/_app/immutable/chunks/B28PfaP6.js": 2526537581472307531, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_linux.h": 15772978805500070727, - "venv/lib/python3.13/site-packages/pandas/_libs/interval.pyi": 15698173755300703836, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/api.py": 9829629144629841277, - "backend/src/core/superset_client/_user_projection.py": 16484323195475306883, - "backend/src/plugins/storage/__init__.py.bak": 7062955918227442388, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py": 2628895559914914693, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/structures.py": 2668010839316715865, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/RECORD": 2963644632913976754, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/numpyconfig.h": 13339221870252345205, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_masked.py": 5380551301916549925, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_inference.py": 16402427186697071465, - "venv/lib/python3.13/site-packages/pygments/lexers/pascal.py": 5992422120590352714, - "frontend/src/lib/components/layout/TopNavbar.svelte": 14489256050586877364, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_indexing.py": 6039808271042864401, - "venv/lib/python3.13/site-packages/pydantic/networks.py": 15767095796055034509, - "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.py": 11621577915097515056, - "venv/lib/python3.13/site-packages/numpy/random/_sfc64.cpython-313-x86_64-linux-gnu.so": 17186695607612591922, - "venv/lib/python3.13/site-packages/numpy/_core/strings.py": 3623885046208831106, - "venv/lib/python3.13/site-packages/pygments/lexers/_googlesql_builtins.py": 12251289884604745945, - "frontend/src/routes/dashboards/health/+page.svelte": 5234355531444674267, - "frontend/src/routes/tools/debug/+page.svelte": 8699044541980877686, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_utils.py": 2329717399120006435, - "frontend/src/services/connectionService.js": 16613277497380191542, - "venv/lib/python3.13/site-packages/pydantic/deprecated/config.py": 12280996227531856230, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/publicmod.f90": 2174121054736811903, - "venv/lib/python3.13/site-packages/pandas/tests/construction/test_extract_array.py": 3100523370314150013, - "venv/lib/python3.13/site-packages/pillow.libs/libjpeg-32d42e18.so.62.4.0": 11717944412021329207, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/shared.py": 1162754506656505196, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_ticks.py": 11649965080303468085, - "frontend/src/components/llm/ProviderConfig.svelte": 1564511833097781020, - "venv/lib/python3.13/site-packages/pygments/lexers/rego.py": 16022844238903541738, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_setitem.py": 14920902525192671548, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_astype.py": 8633811117672837999, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/RECORD": 7266315886702289726, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/WHEEL": 5076688779572520166, - "backend/src/api/routes/git/_repo_lifecycle_routes.py": 6605622512801609889, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/__init__.py": 5027355874704300832, - "venv/lib/python3.13/site-packages/pillow.libs/liblzma-61b1002e.so.5.8.2": 11405063484938458842, - "frontend/src/lib/utils.js": 5436516952434475239, - "backend/src/api/routes/dataset_review_pkg/_routes.py": 11040245149296528009, - "venv/lib/python3.13/site-packages/pydantic/alias_generators.py": 5991580234179623842, - "venv/lib/python3.13/site-packages/pygments/lexers/go.py": 11118104397271513417, - "backend/tests/test_core_scheduler.py": 6589618269087393661, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/scalars.py": 3468820466230647963, - "venv/lib/python3.13/site-packages/pydantic/_internal/_git.py": 8760779092674434712, - "venv/lib/python3.13/site-packages/pygments/lexers/teraterm.py": 16132676290138105984, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine.py": 4446011286411407708, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_copy.py": 3114918520437707410, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayscalars.h": 16142050006935459679, - "venv/lib/python3.13/site-packages/PIL/ImageChops.py": 16368812693381154921, - "backend/src/models/dataset_review_pkg/_finding_models.py": 17608383421420046491, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE": 4672913476475760762, - "venv/lib/python3.13/site-packages/gitdb/stream.py": 3035738824550231197, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite_e.py": 8912795372542950449, - "venv/lib/python3.13/site-packages/pandas/core/groupby/base.py": 16888506204910366302, - "venv/lib/python3.13/site-packages/_yaml/__init__.py": 3395829783751571350, - "venv/lib/python3.13/site-packages/_pytest/setupplan.py": 7496801885995567141, - "venv/lib/python3.13/site-packages/numpy/testing/__init__.py": 2831275254313750978, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_neighborhood_iterator_imp.h": 10634781863364525895, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_federal.py": 16758212916399857971, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rank.py": 14115215895836854973, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/__init__.py": 8632038387100622009, - "venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py": 17461379236189029253, - "venv/lib/python3.13/site-packages/attr/_config.py": 1864824728504480204, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/six.py": 1161259802604920635, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_app.py": 14788764260762890368, - "venv/lib/python3.13/site-packages/pandas/core/tools/numeric.py": 8510988912136285022, - "venv/lib/python3.13/site-packages/anyio/abc/__init__.py": 11428476337116642278, - "venv/lib/python3.13/site-packages/fastapi/middleware/httpsredirect.py": 5775982317459922443, - "venv/lib/python3.13/site-packages/pip/_internal/commands/debug.py": 13528633965821062950, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_compat.py": 11481259393140789881, - "venv/lib/python3.13/site-packages/_pytest/_io/pprint.py": 11202117031437223527, - "venv/lib/python3.13/site-packages/pip/_internal/index/__init__.py": 14829074315307489628, - "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pyi": 15790738442952696034, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_kwarg.py": 10417724544218303733, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/cymysql.py": 5570881524647789741, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_frame_equal.py": 12459816298158655274, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/ecdsa/test_curves.py": 6577762025900435885, - "venv/lib/python3.13/site-packages/pygments/lexers/j.py": 9766594937623918053, - "backend/src/core/task_manager/manager.py": 5748204558787054193, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_pbkdf2.py": 4977067068389993374, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_equals.py": 11041884432990762650, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_chaining_and_caching.py": 11784574265874608859, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_series.py": 2980850649986213051, - "frontend/src/lib/stores/__tests__/sidebar.test.js": 5168138864675110669, - "venv/lib/python3.13/site-packages/greenlet/tests/test_version.py": 6317733744899804292, - "venv/lib/python3.13/site-packages/pandas/io/json/_json.py": 1393095374713367445, - "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.py": 2953876934840155920, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic/functional_validators.py": 5835339743072048940, - "backend/src/api/routes/__tests__/test_dashboards.py": 14899529124453053148, - "venv/lib/python3.13/site-packages/pygments/lexers/jvm.py": 897669925284011089, - "backend/src/api/routes/health.py": 4494840518139445701, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/mongodb.py": 5310116401542377882, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/_arrow_utils.py": 17808805047762566525, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkeyutils-dfe70bd6.so.1.5": 9414227432578104677, - "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/__init__.py": 4870269250323682576, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_win_type.py": 18105114803686556748, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/base.py": 13634605726041717070, - "venv/lib/python3.13/site-packages/pygments/lexers/idl.py": 11454152060351801696, - "venv/lib/python3.13/site-packages/websockets/http.py": 9406523480132204760, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_compat.py": 6032546839876195115, - "venv/lib/python3.13/site-packages/uvicorn/loops/auto.py": 13086005977087619545, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/METADATA": 14608873187265321271, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/base.py": 5294446834530589760, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ufuncobject.h": 5112456258078662871, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_sparse.py": 14343335524508450058, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufuncs.pyi": 1808147181379894521, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas.py": 9446226619493404132, - "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/__init__.py": 9083396324667433335, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_sort_values.py": 15072305514178699398, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_cumulative.py": 11752712818547022872, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_fillna.py": 4610666845291315333, - "build.sh": 37085706669440656, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/METADATA": 15876173086384975957, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_errors.py": 3301889847545579393, - "venv/lib/python3.13/site-packages/pydantic/_internal/_discriminated_union.py": 1818179910730401509, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parse_iso8601.py": 9100947620267156161, - "venv/lib/python3.13/site-packages/pip/_internal/utils/egg_link.py": 18211947508793805393, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.pyi": 16372699564794732463, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connection.py": 8190661510572400096, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_align.py": 10284128457194204574, - "venv/lib/python3.13/site-packages/authlib/oidc/core/claims.py": 2035724920423374302, - "venv/lib/python3.13/site-packages/pandas/io/xml.py": 326466573926392764, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numerictypes.py": 8836108334624807647, - "venv/lib/python3.13/site-packages/pandas/_libs/index.cpython-313-x86_64-linux-gnu.so": 18409579812171549318, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/gh18403_mod.f90": 6219951399575860362, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.cpython-313-x86_64-linux-gnu.so": 7840846965295537844, - "venv/lib/python3.13/site-packages/git/diff.py": 6691274015605451565, - "run.sh": 4808871544409245302, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/__init__.py": 7316739139609490520, - "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.py": 15854257948148516490, - "venv/lib/python3.13/site-packages/numpy/fft/_helper.py": 15906444100774706395, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_join.py": 13115782559805895662, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_set_name.py": 9094608008538024609, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cosh.csv": 9287774220881887502, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_skew.py": 5276102682005383261, - "venv/lib/python3.13/site-packages/psycopg2/_psycopg.cpython-313-x86_64-linux-gnu.so": 13471342289207537479, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/framework_integration.py": 3935346152156572025, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py": 2300277198409164253, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_dataframe.py": 14945138617962156057, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/interfaces.py": 5371646629319169153, - "frontend/src/lib/components/dataset-review/LaunchConfirmationPanel.svelte": 5965510319309867257, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py": 2763962019423787504, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/setup.py": 5482352991573617756, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.py": 15657897994221955882, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_two_greenlets.py": 11156275429806391390, - "backend/src/api/routes/__tests__/test_clean_release_source_policy.py": 15242462571646635922, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numeric_only.py": 13610631681447961840, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/well_known.py": 7183795183137931709, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/queue.py": 7820997545569991670, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_antijoin.py": 14435396877404907826, - "frontend/build/_app/immutable/chunks/C9a28aU5.js": 5513483692277369740, - "backend/src/services/clean_release/repositories/audit_repository.py": 9617813121624417458, - "venv/lib/python3.13/site-packages/numpy/__init__.cython-30.pxd": 1476688171642957054, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/lib/mixins.pyi": 15597536290223628777, - "venv/lib/python3.13/site-packages/numpy/_core/defchararray.py": 15550230459945996915, - "venv/lib/python3.13/site-packages/dotenv/cli.py": 7952654621986067973, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/url.py": 9692934100602135106, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsinh.csv": 13695545125848146013, - "frontend/build/_app/immutable/chunks/Bxli7MEx.js": 13142003860484323974, - "venv/lib/python3.13/site-packages/pygments/lexers/basic.py": 12723021007042054402, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_install.py": 7181005402818830655, - "venv/bin/pyrsa-encrypt": 5858278204727417482, - "frontend/src/lib/components/reports/__tests__/report_card.ux.test.js": 14899927226278257748, - "venv/lib/python3.13/site-packages/numpy/tests/test_configtool.py": 8208415015478205502, - "venv/lib/python3.13/site-packages/pandas/_libs/parsers.pyi": 18402185996588014111, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_other.py": 783701524491896337, - "backend/src/services/clean_release/__tests__/test_stages.py": 15353751625742444422, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_scalar_compat.py": 10068989979144965125, - "venv/lib/python3.13/site-packages/pygments/lexers/dalvik.py": 5549983892200382076, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.f": 9330535808093475892, - "venv/lib/python3.13/site-packages/pandas/util/_validators.py": 10201915528715968165, - "venv/lib/python3.13/site-packages/numpy/_core/shape_base.pyi": 10225243791643474943, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_join.py": 1131792466825856990, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sin.csv": 1443087417808027268, - "venv/lib/python3.13/site-packages/fastapi/middleware/trustedhost.py": 4285622102799027823, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_network.py": 13159427333040109523, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_to_numpy.py": 14845850776625255794, - "backend/src/api/routes/storage.py": 15226523387798112491, - "venv/lib/python3.13/site-packages/numpy/_core/_asarray.py": 3468915833437229425, - "backend/src/services/dataset_review/clarification_engine.py": 13640221555439415988, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dh.py": 5728866271812501558, - "venv/lib/python3.13/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz": 17626265752554406355, - "venv/lib/python3.13/site-packages/fastapi/_compat/shared.py": 5249003014594464718, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/requests.py": 6703055041957748929, - "venv/lib/python3.13/site-packages/idna/intranges.py": 12536174834761591006, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_index.py": 4908727359687606870, - "frontend/src/routes/dashboards/health/__tests__/health_page.integration.test.js": 11653729504599199718, - "backend/src/core/database.py": 7064138501006682572, - "venv/lib/python3.13/site-packages/requests/cookies.py": 4997226307650350876, - "venv/lib/python3.13/site-packages/authlib/jose/util.py": 14843923624043422219, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/METADATA": 9041650844081036822, - "venv/lib/python3.13/site-packages/pip/_internal/cli/parser.py": 1064209424435927674, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/watchfilesreload.py": 13062440477475586751, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_no_deprecated_api.h": 7896028550808252043, - "backend/src/core/async_superset_client.py": 17097960069181151584, - "venv/lib/python3.13/site-packages/click/_winconsole.py": 8133229457213704188, - "venv/lib/python3.13/site-packages/httpcore/_async/interfaces.py": 18250473733460571816, - "frontend/src/routes/settings/automation/+page.svelte": 15129032082642700205, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/instrumentation.py": 13198832283145198959, - "backend/test_pat_api.py": 4206656943734821967, - "venv/lib/python3.13/site-packages/fastapi/templating.py": 10760135144345238340, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE": 2345070715845618109, - "backend/src/models/dataset_review_pkg/_enums.py": 2243691494729792539, - "venv/lib/python3.13/site-packages/keyring/completion.py": 1027436016070040568, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/sql.py": 13315906751371033170, - "venv/lib/python3.13/site-packages/PIL/ImageEnhance.py": 3218405223202808764, - "venv/lib/python3.13/site-packages/cryptography/x509/verification.py": 1079694784860492682, - "docker/backend.Dockerfile": 10320070102635663967, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/scalars.pyi": 3130685119595923378, - "venv/lib/python3.13/site-packages/PIL/ImImagePlugin.py": 15452956038794399492, - "backend/src/models/dataset_review_pkg/_profile_models.py": 3121807680272913669, - "venv/lib/python3.13/site-packages/PIL/_imaging.pyi": 17119084207765761921, - "venv/lib/python3.13/site-packages/pandas/core/arrays/categorical.py": 15993211255162930859, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.py": 15726448028057859831, - "venv/lib/python3.13/site-packages/PIL/TiffImagePlugin.py": 6845043550424565106, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/LICENSE": 6581019367004699816, - "venv/lib/python3.13/site-packages/uvicorn/protocols/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/fastapi/cli.py": 13972351579061154035, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_classes.py": 1440926036579216168, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_downcast.py": 10399746645741452200, - "venv/lib/python3.13/site-packages/pluggy/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_setops.py": 9991233160569430204, - "venv/lib/python3.13/site-packages/pandas/tests/frame/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rpds/py.typed": 15130871412783076140, - "frontend/build/_app/immutable/chunks/YCgcezOr.js": 10021400156442571831, - "frontend/src/components/git/__tests__/git_manager.unfinished_merge.integration.test.js": 2437475749409257490, - "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.py": 13012775703413938576, - "venv/lib/python3.13/site-packages/pygments/lexers/graphql.py": 2307854666015224677, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/util.py": 4890614159013873000, - "venv/lib/python3.13/site-packages/pygments/lexers/arrow.py": 6699009974224818979, - "venv/lib/python3.13/site-packages/starlette/background.py": 10232175205397808417, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/_mapping.py": 17519715342700184302, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/asyncmy.py": 11808138763443525443, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arithmetic.pyi": 467927803068643024, - "venv/lib/python3.13/site-packages/pandas/_libs/arrays.cpython-313-x86_64-linux-gnu.so": 8634234308863090562, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_str.py": 149688376805129532, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_fillna.py": 16297834601116528166, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_datetime.py": 11417836378782978454, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_analytics.py": 8155550493311083506, - "venv/lib/python3.13/site-packages/pydantic/_internal/_core_utils.py": 13248004771466837645, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapped_collection.py": 11377280960650304531, - "venv/lib/python3.13/site-packages/PIL/WalImageFile.py": 12442826195371137238, - "venv/lib/python3.13/site-packages/PIL/_imagingtk.cpython-313-x86_64-linux-gnu.so": 1708198947135304326, - "venv/lib/python3.13/site-packages/PIL/ImageFile.py": 15765130658900167517, - "venv/lib/python3.13/site-packages/git/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/METADATA": 3643456577199395707, - "frontend/src/routes/reports/llm/[taskId]/+page.svelte": 14394593511269407852, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufuncs.py": 12732599294816239111, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/operators.f90": 5949265961908386974, - "venv/lib/python3.13/site-packages/pygments/lexers/hare.py": 8693002231020751711, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/concatkdf.py": 6712732248328458871, - "backend/src/core/utils/superset_context_extractor/_pii.py": 14826864290194304173, - "backend/src/core/superset_client/_dashboards_crud.py": 4664728216214173491, - "backend/src/plugins/translate/__tests__/test_orchestrator.py": 6546216896393785602, - "venv/lib/python3.13/site-packages/pydantic/tools.py": 8931184638883701008, - "venv/lib/python3.13/site-packages/pillow.libs/libXau-154567c4.so.6.0.0": 4340972632724893052, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_os.h": 7883047506449405400, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.py": 17112989332785349705, - "venv/lib/python3.13/site-packages/gitdb/db/git.py": 13552203494737962026, - "venv/lib/python3.13/site-packages/_pytest/warnings.py": 8633394774569709013, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/ecdsa/ecdh.py": 12596317261561755793, - "venv/lib/python3.13/site-packages/numpy/lib/format.pyi": 11752225430594536662, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_regression.py": 9375525053479844400, - "venv/lib/python3.13/site-packages/pandas/core/reshape/pivot.py": 4736553530021653868, - "venv/lib/python3.13/site-packages/authlib/oauth1/errors.py": 1154126353335030046, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/METADATA": 8985010079096958084, - "venv/lib/python3.13/site-packages/packaging/version.py": 13470993734108007779, - "venv/lib/python3.13/site-packages/pyasn1/compat/__init__.py": 5902203086150636670, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_constructors.py": 2439700578107653984, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA": 18410956221497090898, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/licenses/LICENSE": 8364608327598414179, - "venv/lib/python3.13/site-packages/rsa/__init__.py": 14623742580410014505, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_indexing.py": 16623126293364224089, - "venv/lib/python3.13/site-packages/PIL/report.py": 9428754440615681790, - "backend/src/services/clean_release/candidate_service.py": 14044982155441302890, - "venv/lib/python3.13/site-packages/attr/_funcs.py": 5302917072419624193, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/jwk.py": 6458001384702613165, - "venv/lib/python3.13/site-packages/apscheduler/executors/tornado.py": 6635576580306890718, - "backend/src/core/superset_client/_databases.py": 14837101840847724937, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/licenses/LICENSE": 3509878235770224233, - "frontend/build/_app/immutable/chunks/CAvRWKcP.js": 11625217357725761813, - "backend/src/api/routes/__tests__/test_dataset_review_api.py": 17381354465400611694, - "backend/src/services/clean_release/stages/__init__.py": 11216464651705158032, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/METADATA": 2624107638694809013, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_repr.py": 5577699046802107144, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/annotation.py": 15855576199737688124, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py": 7624316342162916928, - "venv/lib/python3.13/site-packages/jsonschema/_keywords.py": 949562792260528762, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/abc.py": 6227782083136374888, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_normalize.py": 7378656958631172603, - "venv/lib/python3.13/site-packages/smmap/test/test_buf.py": 6910367739329016619, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_clip.py": 8087139801274517318, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval_new.py": 3893421355544517336, - "backend/src/models/__tests__/test_clean_release.py": 3448406855108346883, - "backend/tests/test_task_persistence.py": 1764267414712263025, - "venv/lib/python3.13/site-packages/keyring/backends/libsecret.py": 11621088556661567306, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/endpoints.py": 18334891787808122725, - "venv/lib/python3.13/site-packages/pygments/lexers/theorem.py": 15666523910367201285, - "venv/lib/python3.13/site-packages/pluggy/__init__.py": 8269188363563356631, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_rank.py": 5398520257852104723, - "backend/src/services/reports/report_service.py": 17586405613867657190, - "venv/lib/python3.13/site-packages/numpy/tests/test_warnings.py": 3154249157134620649, - "venv/lib/python3.13/site-packages/passlib/handlers/sun_md5_crypt.py": 1949993216707182407, - "venv/lib/python3.13/site-packages/sqlalchemy/event/registry.py": 2857989070743633709, - "venv/lib/python3.13/site-packages/gitdb/util.py": 645937410168583692, - "venv/lib/python3.13/site-packages/websockets/sync/connection.py": 2791040669094103200, - "venv/lib/python3.13/site-packages/pip/_internal/cli/cmdoptions.py": 12107733000019393631, - "venv/lib/python3.13/site-packages/numpy/_typing/__init__.py": 18242238390157333247, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_builtin.py": 74545289993348593, - "venv/lib/python3.13/site-packages/sqlalchemy/__init__.py": 9751988289698484530, - "backend/src/core/auth/__tests__/test_auth.py": 14999763239648705195, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/extending.py": 9859383893070793914, - "venv/lib/python3.13/site-packages/starlette/authentication.py": 10905269573809129547, - "venv/lib/python3.13/site-packages/pandas/tests/frame/common.py": 7426635866622531875, - "backend/src/models/report.py": 9185954411073383239, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_api.py": 6439927798101672423, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/handlers/phpass.py": 16489430001449160264, - "frontend/build/_app/immutable/nodes/22.STjI15IE.js": 217759497864778118, - "backend/src/api/auth.py": 16577622718621592661, - "backend/src/api/routes/dashboards/__init__.py": 15214026957520044598, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_datetimelike.py": 15246580603900811749, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_misc.py": 9190561773853901527, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_set_value.py": 17480833128128893268, - "venv/lib/python3.13/site-packages/pandas/core/window/doc.py": 4204367435727844696, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_py_collections.py": 16825222625411635946, - "frontend/src/components/storage/FileList.svelte": 11661146151896000393, - "venv/lib/python3.13/site-packages/pandas/tests/extension/list/test_list.py": 4538625980063375611, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/__init__.py": 15130871412783076140, - "frontend/src/routes/profile/+page.svelte": 17979719303737794427, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_asymmetric.py": 6903778603779934572, - "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.py": 7352635707860427145, - "venv/lib/python3.13/site-packages/pip/_internal/utils/direct_url_helpers.py": 6075890513530296650, - "venv/lib/python3.13/site-packages/pandas/_libs/pandas_datetime.cpython-313-x86_64-linux-gnu.so": 15874094681262240628, - "frontend/src/lib/ui/Icon.svelte": 2201964532717139995, - "venv/lib/python3.13/site-packages/pip/_internal/main.py": 13711402795965346761, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/tree.py": 4991806303236874829, - "venv/lib/python3.13/site-packages/pip/_internal/network/download.py": 5256731341219268693, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine.py": 16193618152415684421, - "venv/lib/python3.13/site-packages/pandas/tests/base/common.py": 13335865314907577354, - "venv/lib/python3.13/site-packages/annotated_doc/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/yaml/representer.py": 10550136298978225013, - "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.test.js": 7689381541193660594, - "backend/tests/core/test_git_service_gitea_pr.py": 6098719224379185148, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/keyring/devpi_client.py": 3445890176686211982, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_openid.py": 5670082466989878756, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/base.py": 16714704323747942110, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_series.py": 10922287546912569276, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_comparison.py": 4611207880323503098, - "venv/lib/python3.13/site-packages/passlib/crypto/digest.py": 15890463288512025688, - "venv/lib/python3.13/site-packages/pandas/io/formats/printing.py": 3054136648863371285, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/events.py": 13473858896579666058, - "venv/lib/python3.13/site-packages/pyasn1/type/tag.py": 13912273274509186651, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop_duplicates.py": 2397396876872403048, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_datetime64.py": 16272266806084058743, - "venv/lib/python3.13/site-packages/pandas/io/pickle.py": 5418646169167774144, - "venv/lib/python3.13/site-packages/dateutil/parser/isoparser.py": 18015430204844361067, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_shape_base.py": 8837845103338426570, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/compat.py": 9758194415584776667, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py": 1945891789924713537, - "venv/lib/python3.13/site-packages/_pytest/mark/__init__.py": 9343015549300275714, - "venv/lib/python3.13/site-packages/smmap/util.py": 13276170835044840023, - "venv/lib/python3.13/site-packages/pygments/lexers/asc.py": 10744437946030875496, - "frontend/src/routes/dashboards/[id]/components/DashboardGitManager.svelte": 4990524146294551357, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/parameters.py": 1647490964392525933, - "venv/lib/python3.13/site-packages/click/core.py": 16793133181718891193, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/util.py": 6088868370298560146, - "venv/lib/python3.13/site-packages/urllib3/_base_connection.py": 14495079097019980154, - "venv/lib/python3.13/site-packages/requests/hooks.py": 9888227370911151341, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/test_support.pyi": 3893434234020223151, - "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.py": 9409852116555866847, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_size.py": 8527839563560550806, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/bootstrap.py": 3865947239497500815, - "venv/lib/python3.13/site-packages/charset_normalizer/__main__.py": 8887772289570078278, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.asm": 15349921092257597740, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_at_time.py": 15352346149584081864, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_count.py": 16947194157984708529, - "venv/lib/python3.13/site-packages/numpy/tests/test_matlib.py": 14395481530627870882, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isin.py": 8136794647591341784, - "venv/lib/python3.13/site-packages/pip/_internal/network/auth.py": 6159841209618753296, - "venv/lib/python3.13/site-packages/authlib/oauth2/client.py": 7237359250450222680, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_cipheralgorithm.py": 10057637461647348368, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_engines.py": 2790877023773269276, - "venv/lib/python3.13/site-packages/pandas/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/highway/LICENSE": 15636145745140922094, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/RECORD": 16740224557818847141, - "venv/lib/python3.13/site-packages/numpy/core/_dtype.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/uvicorn/middleware/wsgi.py": 9179913252056394958, - "frontend/src/lib/stores/datasetReviewSession.js": 3915919349382526215, - "backend/src/services/__tests__/test_llm_provider.py": 4705572506930455768, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/enumerated.py": 16623643153933348755, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_legacy.py": 12735901522735836481, - "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/expressions.py": 8015786996064244656, - "venv/lib/python3.13/site-packages/pytest_asyncio/__init__.py": 8162213292669110211, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/licenses/LICENSE": 3996324383113221529, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimelike.py": 5317270578347271174, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo77.f": 16250175448122328648, - "venv/lib/python3.13/site-packages/websockets/server.py": 16599051743322571790, - "venv/lib/python3.13/site-packages/pyasn1/__init__.py": 8089201217593738032, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/wheel.py": 13979536782410081712, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/fastapi/_compat/__init__.py": 12380131786677943349, - "venv/lib/python3.13/site-packages/httpcore/_backends/mock.py": 6849131425727092784, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pydantic/_internal/_generics.py": 16438159042939059717, - "venv/lib/python3.13/site-packages/anyio/_core/_synchronization.py": 7050001375718876608, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/resource_protector.py": 11539718751174980004, - "frontend/src/lib/ui/index.ts": 6883965364006817450, - "frontend/src/routes/datasets/+page.svelte": 18132315883450880052, - "frontend/src/components/storage/FileUpload.svelte": 14805293086068620697, - "venv/lib/python3.13/site-packages/numpy/random/_sfc64.pyi": 3536657185495782642, - "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.py": 151202984060101867, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_regression.py": 6742646011852052645, - "venv/lib/python3.13/site-packages/yaml/scanner.py": 8554164423030397366, - "frontend/build/_app/immutable/nodes/1.NnkkqxwF.js": 2557541528204355497, - "venv/lib/python3.13/site-packages/pluggy/_manager.py": 18202550145040079612, - "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/console.py": 3540660073442483692, - "venv/lib/python3.13/site-packages/pydantic/experimental/pipeline.py": 9493054841465227788, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby.py": 16180421154734310873, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/zookeeper.py": 4318643109309626098, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_indexing.py": 81180636019259296, - "venv/lib/python3.13/site-packages/authlib/oidc/discovery/models.py": 6924198551176217533, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_alter_axes.py": 11338712836362534969, - "venv/lib/python3.13/site-packages/pygments/lexers/zig.py": 470431199354307668, - "backend/src/models/dashboard.py": 8479838979214366697, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/__version__.py": 14138134075118474134, - "venv/lib/python3.13/site-packages/requests/structures.py": 2668010839316715865, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nbit_base_example.pyi": 8571452382806614924, - "venv/lib/python3.13/site-packages/numpy/typing/__init__.pyi": 10058149460938883683, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_arithmetics.py": 16527169124402715210, - "venv/lib/python3.13/site-packages/jeepney/wrappers.py": 15234251727654692435, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_api.py": 14120915875574640262, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_msvc.h": 17581467060937454503, - "venv/lib/python3.13/site-packages/pygments/lexers/__init__.py": 4627540245914849670, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending_distributions.pyx": 15302775787158991386, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_take.py": 14969455660160053080, - "venv/lib/python3.13/site-packages/cffi/recompiler.py": 17809252277700094304, - "venv/lib/python3.13/site-packages/sqlalchemy/events.py": 9329355486544822483, - "venv/lib/python3.13/site-packages/pygments/lexers/c_cpp.py": 1211842877043321095, - "venv/lib/python3.13/site-packages/charset_normalizer/md__mypyc.cpython-313-x86_64-linux-gnu.so": 12629680116730190225, - "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.pyi": 5879290708972838025, - "venv/lib/python3.13/site-packages/pygments/lexers/prolog.py": 599539463980699865, - "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py": 11613130235340753050, - "venv/lib/python3.13/site-packages/pygments/lexers/wowtoc.py": 17527844575999013592, - "venv/lib/python3.13/site-packages/pydantic/deprecated/json.py": 17692639396786690144, - "backend/src/api/routes/assistant/_history.py": 16318881016647964852, - "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.pyi": 13945478633722616250, - "backend/src/services/clean_release/repository.py": 3077298775936340703, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_finalize.py": 12539815426478689538, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test__version.py": 9037916251087760255, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_indexing.py": 4063662953863959087, - "backend/src/plugins/translate/metrics.py": 16289306156708185830, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/region.py": 12579403230062395563, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_table.tpl": 17198355067645888735, - "venv/lib/python3.13/site-packages/pygments/lexers/sas.py": 6128008194712914874, - "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/fields.py": 8923326131649837922, - "venv/lib/python3.13/site-packages/pandas/io/sas/sas_xport.py": 16525568795475918874, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_strings.py": 7168544888340296373, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/licenses/LICENSE": 17020744174398828059, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/version.py": 6401479036177132374, - "venv/lib/python3.13/site-packages/ecdsa/__init__.py": 6237633580090653788, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/random.pyi": 9425735458050148191, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/module_data_docstring.f90": 18245403117623015312, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.h": 4516275133897904047, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/text.py": 6096215585499446070, - "venv/lib/python3.13/site-packages/pandas/core/common.py": 14578909424716153057, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_readers.py": 2136472634307645842, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_interval.py": 14749388246573356959, - "venv/lib/python3.13/site-packages/sqlalchemy/log.py": 10101221585967121334, - "backend/src/plugins/git/__init__.py": 2144831474780401625, - "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators_v1.py": 16723743964226526155, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/npyio.pyi": 14147712281086095231, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/base.py": 13269563625127294513, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/const_vs_enum.py": 12873459583943748931, - "venv/lib/python3.13/site-packages/passlib/tests/test_hosts.py": 8284535274820610802, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_arithmetic.py": 17449383588949771785, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_scalar.py": 2434773821902319678, - "backend/src/services/clean_release/policy_engine.py": 3180817867716931725, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/__init__.py": 5896924546560667519, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/errors.py": 12777931960088171310, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pyasn1/codec/der/decoder.py": 11876648884087997745, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_nanfunctions.py": 2356468092336024365, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_month.py": 1613806538807726074, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_round.py": 14996007456708990295, - "venv/lib/python3.13/site-packages/cffi/vengine_gen.py": 6582900613549620556, - "venv/lib/python3.13/site-packages/authlib/integrations/django_client/__init__.py": 14186136918454802843, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nth.py": 17151595277666975085, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/WHEEL": 12054782055241301457, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_swaplevel.py": 3584770792383216626, - "venv/lib/python3.13/site-packages/charset_normalizer/cli/__main__.py": 2148876100501691980, - "venv/lib/python3.13/site-packages/pandas/_version.py": 3480474785419232091, - "venv/lib/python3.13/site-packages/websockets/headers.py": 13587146967490088657, - "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_stat_reductions.py": 11569525436540769152, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_qcut.py": 8972418112914954758, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_cumulative.py": 8178093067175155877, - "venv/lib/python3.13/site-packages/anyio/_core/_tempfile.py": 230288423480899091, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/asymmetric_key.py": 14948661854506285135, - "venv/lib/python3.13/site-packages/websockets/legacy/protocol.py": 762692033188392424, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.cpython-313-x86_64-linux-gnu.so": 1211491156495529145, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/WHEEL": 14550174241288068152, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npz": 12369168483236619421, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_datetime.py": 3499461736474530062, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/models.py": 11521700876708601977, - "venv/lib/python3.13/site-packages/numpy/lib/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/secretstorage/collection.py": 17903258621904912493, - "venv/lib/python3.13/site-packages/anyio/streams/text.py": 12507591237176920223, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_fixed.f90": 8079594129222364031, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_one_public_routine.f90": 7957683453180939605, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_invalid_arg.py": 610891871484326055, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odf.py": 15484940480924122966, - "venv/lib/python3.13/site-packages/fastapi/__main__.py": 10755252341326986079, - "venv/lib/python3.13/site-packages/gitdb/db/pack.py": 13460574575424209609, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/RECORD": 5612689057373270213, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_libsparse.py": 1859997684694949029, - "venv/lib/python3.13/site-packages/pygments/lexers/robotframework.py": 11507713622179102496, - "venv/lib/python3.13/site-packages/pygments/lexers/unicon.py": 1155454077052833821, - "frontend/src/lib/auth/permissions.js": 12787489292887903923, - "backend/src/services/__tests__/test_rbac_permission_catalog.py": 5190751380981933250, - "backend/tests/test_resource_hubs.py": 3990299394875930767, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi": 13016654355866474644, - "venv/lib/python3.13/site-packages/pandas/core/tools/times.py": 6560679305255812809, - "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.pyi": 2186875333484796630, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_append.py": 9644955836191900614, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_integer.py": 16102640589300356639, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_loongarch64_linux.h": 6566098590441297326, - "venv/lib/python3.13/site-packages/pydantic/_internal/_dataclasses.py": 15256193972947054890, - "venv/lib/python3.13/site-packages/ecdsa/ssh.py": 17010933326090778158, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/argon2.py": 15389474901714096890, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py": 17221368219485351394, - "venv/lib/python3.13/site-packages/passlib/tests/sample_config_1s.cfg": 1809713544666311828, - "venv/lib/python3.13/site-packages/fastapi/_compat/model_field.py": 392699021682253569, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_function.py": 644452398244782315, - "backend/src/core/utils/network.py": 2917454689057905472, - "venv/lib/python3.13/site-packages/pygments/lexers/gcodelexer.py": 13712494327007684318, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_repr.py": 13147587049887075335, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_ddl.py": 3690189094298044728, - "venv/lib/python3.13/site-packages/rapidfuzz/utils.py": 5682627611354455144, - "venv/lib/python3.13/site-packages/pydantic/v1/main.py": 213087273729911492, - "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.py": 12796890117561096360, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py": 17873236864291624476, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex.py": 8772423071604000084, - "backend/src/api/routes/git_schemas.py": 9243114525622127446, - "venv/lib/python3.13/site-packages/pandas/core/computation/expr.py": 13085709365673019494, - "venv/lib/python3.13/site-packages/pygments/lexers/oberon.py": 15220143799603451830, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reset_index.py": 5946666595195952693, - "frontend/src/routes/tools/backups/+page.svelte": 6796050751535238723, - "venv/lib/python3.13/site-packages/pandas/io/excel/__init__.py": 6662687963241471393, - "venv/lib/python3.13/site-packages/fastapi/datastructures.py": 727178955746650271, - "frontend/build/_app/immutable/chunks/NoAbU4pR.js": 10213250914533162255, - "venv/lib/python3.13/site-packages/passlib/tests/backports.py": 17098448557554464521, - "venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py": 11015954144633661960, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_arithmetic.py": 15071337112968886738, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/functions.py": 16567226076808693246, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_csky_gcc.h": 12929785267195988209, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/securetransport.py": 12104888897121018676, - "venv/lib/python3.13/site-packages/jose/__init__.py": 15575670403380555945, - "venv/lib/python3.13/site-packages/httpx/_main.py": 1812303705973265093, - "venv/lib/python3.13/site-packages/pandas/_libs/groupby.pyi": 6154952803757623626, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertions.py": 5556667947376168765, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/infer.py": 12541904372617915365, - "venv/lib/python3.13/site-packages/git/objects/submodule/__init__.py": 12958523219496107878, - "venv/lib/python3.13/site-packages/_pytest/unittest.py": 10848803633530338204, - "venv/lib/python3.13/site-packages/pandas/tests/resample/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_range.py": 3544406211185340973, - "venv/lib/python3.13/site-packages/dateutil/relativedelta.py": 5903095963079475484, - "backend/src/api/routes/__tests__/test_assistant_api.py": 16438212076178324033, - "backend/tests/test_smoke_plugins.py": 10704454611361370435, - "venv/lib/python3.13/site-packages/pandas/io/common.py": 12088195203620154171, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/einsumfunc.pyi": 8902765738896588686, - "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.pyi": 8195943561787777127, - "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.py": 1590647503660777156, - "venv/lib/python3.13/site-packages/attr/exceptions.pyi": 12323493441583681402, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/client_auth.py": 3301527187487914220, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.cpython-313-x86_64-linux-gnu.so": 13209491675968970365, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_construction.py": 10619501873883861745, - "venv/lib/python3.13/site-packages/pydantic/dataclasses.py": 12384956801113860889, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/__init__.py": 10470644151952252331, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_common.py": 9097104320321411408, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_get_dummies.py": 16112674981222844375, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/tokens_mixins.py": 9923721909178724437, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.py": 5933622200264572885, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_formats.py": 13752366280743004410, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/expression.py": 15511250785671695359, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/METADATA": 6960169108624303030, - "venv/lib/python3.13/site-packages/websockets/speedups.cpython-313-x86_64-linux-gnu.so": 10941658763526595989, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/persistence.py": 17908437937265384125, - "venv/lib/python3.13/site-packages/dateutil/parser/_parser.py": 9443336890192151397, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/licenses/LICENSE": 12684353321716745791, - "venv/lib/python3.13/site-packages/pydantic/_internal/_serializers.py": 12077180322353709930, - "venv/lib/python3.13/site-packages/pygments/lexers/tact.py": 1164971357219795420, - "venv/lib/python3.13/site-packages/pygments/lexers/markup.py": 10311057264960923433, - "venv/lib/python3.13/site-packages/annotated_doc/main.py": 3384049281532298500, - "venv/lib/python3.13/site-packages/pygments/lexers/compiled.py": 8215290039991205531, - "frontend/src/lib/api/assistant.js": 15470667170083225911, - "backend/src/api/routes/assistant/_schemas.py": 6735060826070731731, - "venv/lib/python3.13/site-packages/passlib/handlers/fshp.py": 13298827601291507295, - "backend/src/api/routes/assistant/__init__.py": 14959302162073618175, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/__init__.py": 3577295959173028042, - "venv/lib/python3.13/site-packages/pandas/_libs/sparse.pyi": 12526865400413362140, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/oracledb.py": 7158791242292470472, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_easter.py": 3397863170068576196, - "backend/src/api/routes/translate/__init__.py": 6357999798616465776, - "venv/lib/python3.13/site-packages/attr/filters.py": 14156141488477438777, - "venv/lib/python3.13/site-packages/numpy.libs/libscipy_openblas64_-096271d3.so": 14338208504131470036, - "venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py": 3893922370396791826, - "venv/lib/python3.13/site-packages/gitdb/pack.py": 7828244200815444166, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pydantic/fields.py": 4142211199816898047, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_io.py": 4199750143323741326, - "venv/lib/python3.13/site-packages/secretstorage/item.py": 15486045386471912693, - "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.pyi": 11535882608355652318, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/linalg.pyi": 7217817172953767893, - "frontend/src/lib/components/translate/TermCorrectionPopup.svelte": 572725570855489866, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py": 14619529283550788275, - "venv/lib/python3.13/site-packages/psycopg2/extras.py": 7510107741729866600, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/__init__.py": 17630019785411546070, - "venv/lib/python3.13/site-packages/pygments/lexers/json5.py": 9736695186548899403, - "backend/src/services/clean_release/manifest_service.py": 10873340336255220063, - "venv/lib/python3.13/site-packages/urllib3/exceptions.py": 9687187566231423953, - "frontend/src/components/TaskHistory.svelte": 7819646903874958430, - "venv/lib/python3.13/site-packages/pandas/tseries/__init__.py": 2648641118117738895, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/METADATA": 1862765380818895200, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_mask.py": 5451866071307236899, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/flatiter.pyi": 9598576095384661517, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_docs.py": 17898499890274685198, - "backend/src/services/clean_release/repositories/manifest_repository.py": 12395698948312301073, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_numeric.py": 18037815278545274402, - "backend/delete_running_tasks.py": 4453015771820664913, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.pyi": 3573629536236108391, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_infer_objects.py": 6769980076977692890, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/RECORD": 8971487317739420905, - "venv/lib/python3.13/site-packages/numpy/_typing/_add_docstring.py": 14657346268890680694, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi": 16934250836612138541, - "venv/lib/python3.13/site-packages/charset_normalizer/cli/__init__.py": 9702005609966176016, - "venv/lib/python3.13/site-packages/pandas/_libs/missing.pyi": 13543139288840944411, - "venv/lib/python3.13/site-packages/numpy/_core/getlimits.py": 7842717073693731093, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_case_when.py": 4133174129660834805, - "venv/lib/python3.13/site-packages/authlib/oauth2/__init__.py": 13682931764896414722, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pop.py": 14885271225856187795, - "venv/lib/python3.13/site-packages/jose/jwt.py": 3405491994268385178, - "venv/lib/python3.13/site-packages/httpx/_utils.py": 4836536051177895209, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_missing.py": 7034102519042351965, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_timedelta.py": 12822713918114715561, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py": 14768815444701392013, - "venv/lib/python3.13/site-packages/cffi/model.py": 13544803366359102016, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_coercion.py": 3551994404797560483, - "venv/lib/python3.13/site-packages/pandas/core/internals/managers.py": 13547912653290063698, - "venv/lib/python3.13/site-packages/pandas/tests/util/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/click/parser.py": 13988544851350599869, - "venv/lib/python3.13/site-packages/httpcore/_sync/http_proxy.py": 14037211594707072958, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/licenses/LICENSE": 7959949171752474553, - "venv/lib/python3.13/site-packages/pygments/lexers/chapel.py": 7619603166448145621, - "venv/lib/python3.13/site-packages/pygments/lexers/kusto.py": 16285533608924436308, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/validator.py": 14200019408839159476, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/__init__.py": 6489437464172324468, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/METADATA": 4868544280955034757, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.py": 1094797446508902066, - "venv/lib/python3.13/site-packages/pip/_internal/models/wheel.py": 10473287225032326206, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/licenses/LICENSE": 8438117004702803716, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/licenses/LICENSE": 3868190977070717994, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api2.pyx": 15936470837420356071, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.cpython-313-x86_64-linux-gnu.so": 1137453907225619959, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/io/orc.py": 12754290195886557668, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/reflection.py": 5951113078754792806, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/properties.py": 5799883762127872919, - "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.cpython-313-x86_64-linux-gnu.so": 3478629877770507853, - "venv/lib/python3.13/site-packages/jeepney/io/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py": 8698341051522816167, - "venv/lib/python3.13/site-packages/pandas/compat/_constants.py": 3909166275649444824, - "venv/lib/python3.13/site-packages/tzlocal/utils.py": 7893867974548929154, - "frontend/build/_app/immutable/chunks/D9dVwiHl.js": 14131011399955207172, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_tree.py": 15789443159423302291, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_npfuncs.py": 13987318550267921389, - "backend/tests/test_auth.py": 6167020718035755646, - "backend/src/services/git/_base.py": 4172145857123113006, - "venv/lib/python3.13/site-packages/httpx/__version__.py": 9473642134297828811, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/pygments/lexers/tls.py": 3552078393009383412, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_axis.py": 4097534573847151853, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_values.py": 12064896795288302730, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_doc.py": 7865575374100528149, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/totp.py": 11888568940387752607, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py": 1422768761337195135, - "venv/lib/python3.13/site-packages/greenlet/TUserGreenlet.cpp": 16258622236210473101, - "venv/lib/python3.13/site-packages/pip/_internal/models/candidate.py": 3099785363867247334, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/py.typed": 9796674040111366709, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_abstract_interface.py": 17625870734108222276, - "venv/lib/python3.13/site-packages/fastapi/routing.py": 466387159689567541, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/licenses/LICENSE": 11199044866758471950, - "venv/lib/python3.13/site-packages/pydantic/experimental/missing_sentinel.py": 1637241331832367709, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/quoted_character/foo.f": 15318083678609435813, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/distributions.h": 17162252287980242413, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.pyi": 14543653817631274394, - "venv/lib/python3.13/site-packages/pandas/_version_meson.py": 4875297137037630519, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/core": 6622984507059115517, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex.py": 16348619731902304844, - "venv/lib/python3.13/site-packages/click/decorators.py": 2920877667855377423, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/wheel.py": 828222664427570058, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_asof.py": 8656532774394927388, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_dst.py": 12243642467695371088, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_http_headers.py": 13187746870209705951, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_iterrows.py": 3791886201566670087, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_quoting.py": 1147992698412929658, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_astype.py": 15384141282611151560, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.pyx": 649821942746469002, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-2.csv": 16611724551361692999, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_nat.py": 18428782698385974380, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/RECORD": 2024211292009381421, - "venv/lib/python3.13/site-packages/PIL/SpiderImagePlugin.py": 12234762087257163606, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_any_index.py": 10800968419286329268, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayterator.pyi": 15722265241660279277, - "venv/lib/python3.13/site-packages/pygments/lexers/_mysql_builtins.py": 16087227298679432006, - "frontend/src/lib/utils/debounce.js": 16884470786640632815, - "backend/logs/app.log.2": 6692440785424103264, - "venv/lib/python3.13/site-packages/pygments/lexers/other.py": 6893672181003034910, - "venv/lib/python3.13/site-packages/cffi/verifier.py": 8473599757770867754, - "venv/lib/python3.13/site-packages/pandas/_libs/lib.pyi": 3049004444338566197, - "scripts/scan_secrets.sh": 599173385111063848, - "backend/src/schemas/dataset_review_pkg/_composites.py": 1898056130177244572, - "venv/lib/python3.13/site-packages/pandas/core/strings/accessor.py": 14021462534679430426, - "backend/tests/services/dataset_review/test_superset_matrix.py": 5114797134613078913, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/resource_protector.py": 9147671710440653567, - "venv/lib/python3.13/site-packages/pygments/lexers/dsls.py": 11696359794961833087, - "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_config.py": 3636377988901907424, - "venv/lib/python3.13/site-packages/pandas/util/_doctools.py": 3766479022075611396, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_extension_array_equal.py": 16119106336497630796, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/util.py": 10640907133050862176, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/roles.py": 5448455900674232810, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py": 8577756801858495168, - "venv/lib/python3.13/site-packages/fastapi/param_functions.py": 14585101566697773072, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_ufunc.py": 5025605639221470838, - "venv/lib/python3.13/site-packages/_pytest/terminal.py": 885450969703476729, - "venv/lib/python3.13/site-packages/pandas/_libs/index.pyi": 17679349633640606985, - "backend/tests/services/clean_release/test_demo_mode_isolation.py": 1970652424726919519, - "venv/lib/python3.13/site-packages/_pytest/_py/error.py": 2257336434271433543, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/LICENSE": 16343563559291870811, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_constructors.pyi": 164538008396001632, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/implicit.py": 5400555157488808948, - "venv/lib/python3.13/site-packages/pygments/lexers/ampl.py": 4594260378653369985, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py": 15377043158761455858, - "venv/lib/python3.13/site-packages/attr/_version_info.py": 3850449653182103457, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_block_docstring.py": 18146459246837890489, - "venv/lib/python3.13/site-packages/PIL/ImageWin.py": 13635870584232081323, - "venv/lib/python3.13/site-packages/sqlalchemy/event/__init__.py": 16182540084654095394, - "venv/lib/python3.13/site-packages/pandas/core/groupby/groupby.py": 5536922766239875007, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_set.py": 14375107704596905983, - "venv/lib/python3.13/site-packages/pyasn1/error.py": 11036037300994516747, - "venv/lib/python3.13/site-packages/urllib3/response.py": 1987354357306756790, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/background.py": 2105814165112049750, - "venv/lib/python3.13/site-packages/websockets/speedups.c": 11800200877763464246, - "venv/lib/python3.13/site-packages/pyasn1/type/tagmap.py": 14075859042659927673, - "venv/lib/python3.13/site-packages/websockets/sync/router.py": 10335595955948355382, - "venv/lib/python3.13/site-packages/pip/_internal/commands/wheel.py": 16397273591245583356, - "venv/lib/python3.13/site-packages/jose/backends/native.py": 50023158618000658, - "venv/lib/python3.13/site-packages/passlib/tests/sample1c.cfg": 13088198405762342506, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_string.py": 13234624995755549329, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_cat_accessor.py": 4304062895283374927, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log.csv": 221209423454319618, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_version.pyi": 3636944488233043883, - "venv/lib/python3.13/site-packages/sqlalchemy/schema.py": 16029542421991709659, - "venv/lib/python3.13/site-packages/pycparser/__init__.py": 16499940957411260458, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_between.py": 337429135216881486, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/models.py": 16636119441941357281, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA": 7788873460115135563, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_time_series.py": 5205199092955498507, - "frontend/src/components/tasks/LogEntryRow.svelte": 10992388311554587653, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/yaml.py": 17801117405950663611, - "backend/src/services/dataset_review/clarification_pkg/_helpers.py": 4822867945130807759, - "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.py": 3001896031623204147, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/accessor.py": 8373575175447947401, - "venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py": 7899068668225389287, "venv/lib/python3.13/site-packages/pandas/core/arrays/floating.py": 1215586266958421991, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/rule.py": 5318344538761246105, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/meson.build": 8784756284679859367, - "venv/lib/python3.13/site-packages/pygments/lexers/php.py": 17636450950688953288, - "venv/lib/python3.13/site-packages/pygments/lexers/amdgpu.py": 17249439604111838979, - "backend/test_auth.db": 18214842050920965999, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/modes.py": 6832635056228445650, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_seed_sequence.py": 7687465232954593311, - "venv/lib/python3.13/site-packages/pandas/io/clipboards.py": 5183212433608887727, - "venv/lib/python3.13/site-packages/fastapi/__init__.py": 11054203163075417954, - "frontend/src/lib/api/__tests__/reports_api.test.js": 11820587361929895100, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_datetime.py": 1678885152993000592, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE": 17284252174641192866, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py": 9615571808720893016, - "venv/lib/python3.13/site-packages/click/types.py": 6491651131669287534, - "venv/lib/python3.13/site-packages/pandas/core/computation/ops.py": 18250656489649699560, - "venv/lib/python3.13/site-packages/pyasn1/codec/cer/decoder.py": 17559787396729804535, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py": 5324490103277723813, - "venv/lib/python3.13/site-packages/jaraco/classes/py.typed": 15130871412783076140, - "frontend/src/components/git/GitManager.svelte": 10046788495793334635, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/RECORD": 9803861100978586236, - "frontend/src/lib/stores/__tests__/test_taskDrawer.js": 13582869875796637016, - "backend/src/services/clean_release/repositories/__init__.py": 9926779835387630811, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/RECORD": 12015288851076448205, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/f2cmap/isoFortranEnvMap.f90": 15768643271492041853, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/PIL/BmpImagePlugin.py": 16682567630621529903, - "venv/lib/python3.13/site-packages/numpy/typing/tests/test_typing.py": 10311989961588249147, - "venv/lib/python3.13/site-packages/pandas/io/stata.py": 14370692318835340323, - "frontend/src/lib/stores/__tests__/test_datasetReviewSession.js": 12620073136419991103, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/base_key.py": 10402800587070352851, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.pyi": 9629457630163613604, - "venv/lib/python3.13/site-packages/pydantic/aliases.py": 14481980467204711659, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_comparisons.py": 2764694394636775220, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop_duplicates.py": 5874427993953991216, - "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.py": 6205675182996213576, - "venv/lib/python3.13/site-packages/pandas/core/util/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html.tpl": 12550218314797699304, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiarray.py": 5928288171180750841, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_array.py": 16867692337986161626, - "venv/lib/python3.13/site-packages/PIL/AvifImagePlugin.py": 1872412416505003364, - "backend/src/scripts/seed_permissions.py": 12022710984424584961, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_join.py": 11910166566689889389, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets.py": 4499538899492148499, - "backend/src/core/auth/__init__.py": 16435384828055133073, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libk5crypto-b1f99d5c.so.3.1": 10669523137596317954, - "venv/lib/python3.13/site-packages/pydantic/_internal/_utils.py": 10936299012115637790, - "venv/lib/python3.13/site-packages/sqlalchemy/event/attr.py": 5971663429221923939, - "venv/lib/python3.13/site-packages/pygments/lexers/codeql.py": 4254046699692048717, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/endpoint.py": 8519226897299461194, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/ma/LICENSE": 6723325434476453127, - "venv/lib/python3.13/site-packages/numpy/_core/umath.pyi": 11775804030935134661, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_contains.py": 5273759677032634868, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/METADATA": 6438256145634479451, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_matrix_linalg.py": 2858987911347541177, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py": 9888795404090331319, - "venv/lib/python3.13/site-packages/jeepney/tests/test_low_level.py": 3579024079580951234, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/codec.py": 14814999235499530522, - "venv/lib/python3.13/site-packages/numpy/_core/_internal.py": 13056946834347643109, - "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.cpython-313-x86_64-linux-gnu.so": 11074560904305737689, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/testing.pyi": 16263777835572515739, - "venv/lib/python3.13/site-packages/pandas/core/window/online.py": 13551140636252302201, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict.py": 808180749933361743, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_formats.py": 1265319125235346745, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiprocessing.py": 5522490547285108448, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_diff.py": 2216748778018882333, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/naming.py": 14503932124671438340, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/RECORD": 6349071557579241461, - "venv/lib/python3.13/site-packages/pygments/lexers/iolang.py": 10997512999728724130, - "venv/lib/python3.13/site-packages/numpy/typing/tests/test_runtime.py": 12534621594420844770, - "frontend/src/types/dashboard.ts": 9718666955722367566, - "frontend/src/components/git/DeploymentModal.svelte": 6232413208557068848, - "venv/lib/python3.13/site-packages/jsonschema/validators.py": 3832874295500213309, - "venv/lib/python3.13/site-packages/jose/exceptions.py": 8562012933958941039, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_integrity.py": 12010564145080323987, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo90.f90": 58572216018544419, - "venv/lib/python3.13/site-packages/sqlalchemy/util/compat.py": 16148553252010089561, - "backend/src/services/notifications/__init__.py": 13930898146447654299, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/tablegen.py": 2917626373516779857, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_time.py": 10463741033950978270, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/jeepney/tests/test_wrappers.py": 4813328996575597980, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/warnings_and_errors.pyi": 9019577563960071498, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_repeat.py": 16971624301893342868, - "venv/lib/python3.13/site-packages/secretstorage/dhcrypto.py": 8860116996905452758, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunc_config.py": 14728417910466146460, - "venv/lib/python3.13/site-packages/passlib/handlers/md5_crypt.py": 7669328623914474137, - "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimelike.py": 4356700619632833878, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/WHEEL": 13232065379159720345, - "venv/lib/python3.13/site-packages/PIL/py.typed": 15130871412783076140, - "frontend/tailwind.config.js": 13139381085024634801, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/visitors.py": 5190923415390076080, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/__init__.py": 6532094538918910232, - "venv/lib/python3.13/site-packages/pandas/tests/computation/test_eval.py": 2921337784087808487, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/METADATA": 1684451390569121499, - "venv/lib/python3.13/site-packages/rsa/parallel.py": 4968566722541778371, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/licenses/LICENSE": 16147999986886816451, - "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector.py": 15458248567257759500, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reshape.py": 10576230966727810832, - "frontend/src/lib/stores/__tests__/mocks/state.js": 3616783395254238777, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/base.py": 11386927749822539057, - "frontend/src/services/toolsService.js": 1568142461283466080, - "venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py": 10408538054406276433, - "venv/lib/python3.13/site-packages/pygments/formatter.py": 11156733836937309465, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarmath.py": 9762951701352556447, - "backend/tests/test_logger.py": 11506419828617508917, - "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/__init__.py": 4290669519959060720, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/RECORD": 9678861487447993169, - "frontend/src/lib/components/layout/__tests__/test_topNavbar.svelte.js": 11053755879172333925, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/memory.py": 9947113720821563254, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_common.py": 2223069451949206319, - "frontend/src/lib/components/translate/TranslationPreview.svelte": 192144965481842186, - "venv/lib/python3.13/site-packages/pip/_internal/network/utils.py": 13421242949994555055, - "backend/src/services/clean_release/publication_service.py": 5215271639026811180, - "venv/lib/python3.13/site-packages/apscheduler/triggers/combining.py": 1264860868414723829, - "venv/lib/python3.13/site-packages/numpy/ma/extras.py": 7559838309481824664, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply.py": 8103417303073393969, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_api.py": 1910541672533352625, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_timegrouper.py": 276805119790266454, - "venv/lib/python3.13/site-packages/_pytest/deprecated.py": 5452408732344157294, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler_py.py": 16189204441438239231, - "venv/lib/python3.13/site-packages/pip/_internal/commands/install.py": 17631777189475433503, - "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.py": 7446626038222262489, - "venv/lib/python3.13/site-packages/greenlet/platform/setup_switch_x64_masm.cmd": 8473866644286785815, - "venv/lib/python3.13/site-packages/numpy/random/_pcg64.pyi": 14785496840737986355, - "venv/lib/python3.13/site-packages/requests/_internal_utils.py": 4091305333167213279, - "backend/src/api/routes/translate/_helpers.py": 13265933139767815312, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/METADATA": 14070889071949105584, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_shape_base.py": 4117141349022217163, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/block_docstring/foo.f": 2489756567993745076, - "venv/lib/python3.13/site-packages/greenlet/TPythonState.cpp": 12828594265749606443, - "venv/lib/python3.13/site-packages/fastapi/utils.py": 15402403831700548944, - "venv/lib/python3.13/site-packages/itsdangerous/url_safe.py": 13454753900702274266, - "venv/lib/python3.13/site-packages/gitdb/__init__.py": 15381521749253695923, - "venv/lib/python3.13/site-packages/pydantic/experimental/__init__.py": 492399139963056461, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_salsa.py": 1756536674254931838, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcrypto-2e26a48f.so.3": 3658133521245392852, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_transform.py": 6759417099641034692, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_missing.py": 2976310411820303374, - "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.py": 546976622482819095, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_julian_date.py": 6868080396693831272, - "venv/lib/python3.13/site-packages/pandas/core/indexes/range.py": 3838277376774246164, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_lib.py": 6721066015604956469, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_clearing_run_switches.py": 3020294789560975101, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_size.py": 16658280641516916790, - "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.pyi": 16799580742094025110, - "venv/lib/python3.13/site-packages/pillow.libs/libavif-01e67780.so.16.3.0": 13947441788826399574, - "venv/lib/python3.13/site-packages/pyasn1/codec/der/encoder.py": 4790267438222037867, - "venv/lib/python3.13/site-packages/git/objects/__init__.py": 2110615482320355676, - "venv/lib/python3.13/site-packages/urllib3/py.typed": 12220177918227495093, - "frontend/eslint.config.js": 14296985543214702398, - "backend/src/core/mapping_service.py": 15428037653286485748, - "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py": 6773352403538028864, - "frontend/build/_app/immutable/chunks/7w4QGhl2.js": 163809850003626570, - "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.py": 16859854811012717088, - "venv/lib/python3.13/site-packages/websockets/exceptions.py": 12048063047468402740, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/METADATA": 16498441272591167134, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py": 12775850858284584296, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_types.py": 4083297168618981717, - "venv/lib/python3.13/site-packages/PIL/ImageMorph.py": 13861961228125789866, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/METADATA": 13776251749114562660, - "venv/lib/python3.13/site-packages/uvicorn/middleware/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/vector.py": 9941185556118343263, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_libfrequencies.py": 16302676625743291594, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_cpu.h": 13799366908726531089, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_deprecations.py": 14624516389510242394, - "venv/lib/python3.13/site-packages/tzlocal/win32.py": 1949985224569608978, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authenticate_client.py": 16886645731942472285, - "venv/lib/python3.13/site-packages/rsa/pkcs1_v2.py": 3533398238491175229, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/sdist.py": 9490366272695301385, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_index_new.py": 796607958391882182, - "venv/lib/python3.13/site-packages/click/_utils.py": 16491408631876616271, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_produces_warning.py": 750781117570375777, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/exceptions.py": 14982764273485726199, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_period.py": 6047041810867211174, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_accessor.py": 10074496835594855786, - "venv/lib/python3.13/site-packages/pandas/io/api.py": 8275376846208437741, - "venv/lib/python3.13/site-packages/dateutil/zoneinfo/__init__.py": 10261654903106416888, - "venv/lib/python3.13/site-packages/click/_termui_impl.py": 3296085065310341855, - "venv/lib/python3.13/site-packages/gitdb/typ.py": 16374739906244805047, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py": 71159121705871510, - "venv/lib/python3.13/site-packages/numpy/_core/_simd.pyi": 13553578040186939249, - "venv/lib/python3.13/site-packages/passlib/utils/pbkdf2.py": 15802959653228956404, - "venv/lib/python3.13/site-packages/git/objects/fun.py": 15014209679156260865, - "venv/lib/python3.13/site-packages/pygments/lexers/snobol.py": 414946539602526513, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-1.csv": 5708101386018266998, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype.py": 7796703383405149774, - "venv/lib/python3.13/site-packages/PIL/MpoImagePlugin.py": 644327650732339301, - "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimelike.py": 394614958716301173, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/__init__.py": 15130871412783076140, - "frontend/src/routes/storage/+page.svelte": 5003283673039328108, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.py": 13115032674230220791, - "backend/src/api/routes/git/_router.py": 8701683910668868827, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_einsum.py": 17286384318722237180, - "venv/lib/python3.13/site-packages/httpcore/_async/connection.py": 2155744160496556069, - "venv/bin/websockets": 12099863324306500376, - "dist/docker/frontend.v1.0.0-rc2-docker.tar": 1705027241971137937, - "venv/lib/python3.13/site-packages/psycopg2/errors.py": 10441360078612452560, - "venv/lib/python3.13/site-packages/_pytest/capture.py": 17850967053989495422, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/sqlalchemy.py": 811968833735951956, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/random.py": 12722750410117529986, - "venv/lib/python3.13/site-packages/passlib/apache.py": 2357244937629445006, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine_first.py": 3100691245280812910, - "frontend/src/components/llm/ValidationReport.svelte": 11810571826279890996, - "venv/lib/python3.13/site-packages/pyasn1/codec/streaming.py": 13202773554072937140, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/tools.py": 6437315459200419827, - "venv/lib/python3.13/site-packages/pandas/_testing/_warnings.py": 5640964566228306285, - "venv/lib/python3.13/site-packages/pillow.libs/libsharpyuv-95d8a097.so.0.1.2": 5320647379976541240, - "venv/lib/python3.13/site-packages/pandas/io/sas/sasreader.py": 12766780210114366112, - "venv/lib/python3.13/site-packages/pygments/lexers/julia.py": 13516407944275342328, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/util.py": 5252158959761945519, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_compression.py": 3167245427092658522, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/datasource.pyi": 80441145804744162, - "venv/lib/python3.13/site-packages/pandas/_libs/reshape.cpython-313-x86_64-linux-gnu.so": 10639750501333255943, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_reductions.py": 633644255158859565, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libselinux-0922c95c.so.1": 16516617710645376769, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_value_attrspec.py": 7741590618654448240, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/provision.py": 2676876852726658063, - "venv/lib/python3.13/site-packages/_pytest/terminalprogress.py": 5490989904517343259, - "docker/backend.entrypoint.sh": 16079402358996730188, - "backend/src/core/superset_profile_lookup.py": 7009220677319131100, - "frontend/src/lib/components/reports/reportTypeProfiles.js": 10752964062056890725, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_mangle_dupes.py": 15613858733226279352, - "venv/lib/python3.13/site-packages/urllib3/contrib/socks.py": 645086578806871685, - "venv/lib/python3.13/site-packages/pydantic/v1/color.py": 14545509649623975912, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/RECORD": 12555714734722889695, - "backend/logs/app.log.1": 5033969853318871181, - "venv/lib/python3.13/site-packages/anyio/lowlevel.py": 11919467357746648592, - "venv/lib/python3.13/site-packages/pip/_internal/commands/check.py": 6937252393012787214, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py": 17520043776344667309, - "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.pyi": 12052895927933479005, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/fromnumeric.pyi": 6708743411405679061, - "venv/lib/python3.13/site-packages/pygments/lexers/apl.py": 16767474673619335683, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunclike.pyi": 14623308681600207918, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/__init__.py": 15130871412783076140, - "frontend/src/routes/+page.svelte": 5766160745549070101, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/_gen_files.py": 6273314671315377131, - "venv/lib/python3.13/site-packages/PIL/McIdasImagePlugin.py": 14137545069906812251, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/negative_bounds/issue_20853.f90": 3911644649277659333, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/introspection.py": 14645217065508644154, - "venv/bin/activate.csh": 14513616043256836238, - "frontend/src/routes/settings/notifications/+page.svelte": 14504217413720856827, - "frontend/src/components/auth/ProtectedRoute.svelte": 7705373237339863677, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-2.csv": 4052796393463357888, - "frontend/src/lib/components/layout/Breadcrumbs.svelte": 17883872226488804922, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ranges.py": 14549386056660231279, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_accuracy.py": 3904008607472505376, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_insert.py": 9195027887301180927, - "venv/lib/python3.13/site-packages/httpx/_transports/__init__.py": 5931944625631942279, - "venv/lib/python3.13/site-packages/apscheduler/util.py": 11440701924954413440, - "venv/lib/python3.13/site-packages/gitdb/db/__init__.py": 2615504918450276693, - "venv/lib/python3.13/site-packages/_pytest/_code/code.py": 5248836309618416369, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/base.py": 7110726987481713080, - "venv/lib/python3.13/site-packages/pip/__main__.py": 574438877257676911, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_concat.py": 1351478313334379336, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/writeonly.py": 14213960431308620600, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/toml.py": 2225382735669153136, - "venv/lib/python3.13/site-packages/numpy/strings/__init__.pyi": 6213864891045042045, - "venv/lib/python3.13/site-packages/pandas/io/formats/string.py": 14718123345899686696, - "venv/lib/python3.13/site-packages/pygments/lexers/csound.py": 2175454840368374515, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.py": 3762934316884129514, - "venv/bin/httpx": 6651007446865716794, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/client.py": 18443944554871937480, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/RECORD": 8956798822782402202, - "venv/bin/pip3": 13861749540792881808, - "venv/lib/python3.13/site-packages/pandas/tests/generic/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_indexing.py": 16264430536704066847, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append_common.py": 2377351120083195530, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/jupyter.py": 4446535289156468239, - "venv/lib/python3.13/site-packages/pandas/tseries/holiday.py": 1134358927599488799, - "venv/lib/python3.13/site-packages/uvicorn/protocols/utils.py": 6147480543268737542, - "venv/lib/python3.13/site-packages/pygments/lexers/_php_builtins.py": 2344562378679281201, - "venv/lib/python3.13/site-packages/pygments/lexers/esoteric.py": 16839499623020685146, - "backend/src/services/git/_merge.py": 16183885992149357137, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_box_unbox.py": 7579587620530953711, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/METADATA": 14126682758562488244, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/filesize.py": 13234856188049714485, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_sort.py": 11941630642630249576, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/signature.py": 3614545297259098566, - "venv/lib/python3.13/site-packages/numpy/ma/README.rst": 13203411621071845216, - "venv/lib/python3.13/site-packages/numpy/f2py/setup.cfg": 17974180080760231128, - "venv/lib/python3.13/site-packages/click/globals.py": 18353671282454331523, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_timezones.py": 4771674179164304731, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.py": 14865906347579451546, - "venv/lib/python3.13/site-packages/pygments/lexers/ldap.py": 17455983150181177257, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/WHEEL": 5838667082726674838, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayprint.pyi": 2424868797160722318, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_numeric.py": 17391095502314929042, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/computation/engines.py": 1889823242704769952, - "venv/lib/python3.13/site-packages/greenlet/greenlet_internal.hpp": 13173115501223013762, - "venv/lib/python3.13/site-packages/greenlet/TExceptionState.cpp": 17617354576371846109, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_constructors.py": 17392287676196409733, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nlargest_nsmallest.py": 11049656379959696695, - "venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py": 13206297306768491360, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo90.f90": 2435648607844815947, - "venv/lib/python3.13/site-packages/pandas/core/groupby/ops.py": 2036968385544457016, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows.py": 13341168721223961872, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd.py": 7577408467041941595, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/putmask.py": 10934101690788549526, - "venv/lib/python3.13/site-packages/passlib/ifc.py": 14479133654556657428, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/shape.py": 13410030168622484801, - "venv/lib/python3.13/site-packages/numpy/strings/__init__.py": 6523674574341354095, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/__init__.py": 12975130533860125272, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_structures.py": 16375837477294135345, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_symbol.py": 10792387279408528886, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_xs.py": 11073752361562791165, - "venv/lib/python3.13/site-packages/pip/_internal/cli/autocompletion.py": 7651919329635563739, - "venv/lib/python3.13/site-packages/numpy/core/overrides.py": 11228841530565061867, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_sequence.py": 3721630758772772345, - "scripts/generate_financial_arrears_data.py": 15989529185799374517, - "venv/lib/python3.13/site-packages/pandas/core/computation/scope.py": 16859504937881548797, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/data.f90": 13833561828029901615, - "frontend/src/lib/stores/environmentContext.js": 1762535408632658714, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapper.py": 16715864918034312591, - "frontend/build/_app/immutable/chunks/D9yKQnUv.js": 14194129519610241122, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.cpython-313-x86_64-linux-gnu.so": 13104135217735363438, - "frontend/build/_app/immutable/nodes/7.DPFqrug7.js": 12760418087468020149, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_msvc.h": 2465089365642299646, - "venv/lib/python3.13/site-packages/pydantic/v1/_hypothesis_plugin.py": 6383299780629629080, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/LICENSE": 11167201188673981085, - "venv/lib/python3.13/site-packages/urllib3/util/response.py": 6106698559175827207, - "frontend/vite.config.js": 6182424215365912367, - "frontend/src/lib/stores/sidebar.js": 8409084311045055153, - "backend/src/core/auth/jwt.py": 2300190538212143465, - "backend/src/api/routes/datasets.py": 10960880516483380521, - "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.pyi": 8232620851756907417, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/orderinglist.py": 10162099108656794338, - "venv/include/site/python3.13/greenlet/greenlet.h": 16216120538647882082, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml.py": 6906547816798138000, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_repr.py": 11737029058271003097, - "venv/lib/python3.13/site-packages/pygments/lexers/grammar_notation.py": 9009644754106884095, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/WHEEL": 9788895026336324100, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayobject.h": 4345432890284821918, - "venv/lib/python3.13/site-packages/pygments/sphinxext.py": 2768161754523156130, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_use.f90": 2657057181044959432, - "venv/lib/python3.13/site-packages/websockets/legacy/http.py": 16266924390281399720, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/dtype.pyi": 465999539608816848, - "venv/lib/python3.13/site-packages/pygments/lexers/_ada_builtins.py": 16331776886501464264, - "venv/lib/python3.13/site-packages/httpcore/_backends/sync.py": 576247220834179928, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/git/objects/tree.py": 10239027037919781330, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/__init__.py": 11526998905763262175, - "venv/lib/python3.13/site-packages/gitdb/test/test_util.py": 541144638852210407, - "venv/lib/python3.13/site-packages/pygments/lexers/shell.py": 3842338968014057744, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/keyring/backend_complete.zsh": 14339032329370660432, - "venv/lib/python3.13/site-packages/rsa/key.py": 4783269051050521475, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp.csv": 601782991752922510, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/METADATA": 14543822270526146783, - "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/test_rapidfuzz_packaging.py": 5189055608193366093, - "venv/lib/python3.13/site-packages/numpy/_utils/__init__.py": 7731065084036767848, - "venv/lib/python3.13/site-packages/uvicorn/lifespan/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/_sql_builtins.py": 5906655440517434734, - "venv/lib/python3.13/site-packages/anyio/abc/_sockets.py": 15042495744291042062, - "venv/lib/python3.13/site-packages/attr/converters.pyi": 17675091788614033166, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/LICENSE": 8423899413955829149, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_encs.py": 9703572751543253904, - "venv/lib/python3.13/site-packages/numpy/_core/lib/pkgconfig/numpy.pc": 14713682399742251335, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/conftest.py": 6137462119729731945, - "docker/all-in-one.Dockerfile": 10896793266219904099, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/cffi/cffi_opcode.py": 8411344362428947268, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_formats.py": 13500155167624433622, - "venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py": 15555781487631855604, - "venv/lib/python3.13/site-packages/ecdsa/ecdsa.py": 9770689913380927623, - "venv/lib/python3.13/site-packages/httpx/_transports/base.py": 6498990531443644896, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_ndarray.py": 6863874753941288585, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_common.py": 12064185052519470358, - "venv/lib/python3.13/site-packages/greenlet/tests/__init__.py": 1181423074160870921, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_month.py": 8485374160446988786, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_cumulative.py": 16283954014170928273, - "venv/lib/python3.13/site-packages/pycparser/_c_ast.cfg": 2649342580905806389, - "venv/lib/python3.13/site-packages/jaraco/classes/meta.py": 15930255620248873425, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arrayterator.py": 1646530870049791291, - "venv/lib/python3.13/site-packages/cryptography/fernet.py": 10137792148038140630, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py": 6509914623926583629, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_replace.py": 8784361132920473760, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np126.pkl.gz": 9677994062701347206, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/RECORD": 15574202654585730126, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/dictionary.py": 9599439750713575741, - "venv/lib/python3.13/site-packages/pygments/styles/autumn.py": 8750937312198920971, - "venv/lib/python3.13/site-packages/pygments/lexers/openscad.py": 13045825115225232396, - "venv/bin/py.test": 2593802493707545818, - "backend/src/core/__tests__/test_superset_preview_pipeline.py": 5338719285630543394, - "venv/lib/python3.13/site-packages/pygments/lexers/cplint.py": 13990118342267278047, - "backend/src/core/migration_engine.py": 1303635539349402052, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/selectable.py": 14646717349365560549, - "backend/src/services/git/_status.py": 8680190871452171795, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_s390_unix.h": 5161826643658505126, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/accessors.py": 12743680296510533030, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_coercion.py": 7261787308019145312, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_str_accessor.py": 4066094997616994612, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_comparison.py": 5348781442514871089, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polyutils.pyi": 3841660741524427413, - "venv/lib/python3.13/site-packages/git/compat.py": 16843345645628515492, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/twisted.py": 16059862619165885730, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/__init__.py": 2345997696115514124, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/flow_control.py": 4822804840057360506, - "venv/lib/python3.13/site-packages/PIL/_avif.cpython-313-x86_64-linux-gnu.so": 5243449968857258659, - "backend/src/core/scheduler.py.bak": 15045927989195339728, - "venv/lib/python3.13/site-packages/keyring/backends/macOS/api.py": 4502902558534812719, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_between_time.py": 14266333058913745751, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS": 2987771679926243782, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get.py": 6509102681661636928, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/WHEEL": 2357997949040430835, - "frontend/build/_app/immutable/chunks/CTtgwBHs.js": 9488056280956425328, - "venv/lib/python3.13/site-packages/py.py": 16248211404003181569, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.BSD": 16833702494864671773, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/warnings_and_errors.py": 11172336980866895356, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_explode.py": 18263694040596159591, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_construction.py": 13953424340346625390, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_xport.py": 14080914637071212225, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/getlimits.pyi": 93747984191461691, - "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.py": 13574106461973044500, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_indexing.py": 4334578480714520407, - "venv/lib/python3.13/site-packages/jeepney/io/asyncio.py": 13239992245695516007, - "venv/lib/python3.13/site-packages/pillow.libs/liblcms2-cc10e42f.so.2.0.17": 4221600618602131476, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py": 7019253290755243568, - "venv/lib/python3.13/site-packages/pydantic/annotated_handlers.py": 10186120300163345360, - "venv/lib/python3.13/site-packages/authlib/common/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/histograms.pyi": 9255620158831643201, - "venv/lib/python3.13/site-packages/pandas/core/interchange/from_dataframe.py": 8278998381856028462, - "venv/lib/python3.13/site-packages/pandas/tests/io/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/multipart/multipart.py": 26386459953377432, - "venv/lib/python3.13/site-packages/pyasn1/type/char.py": 16518487114777185541, - "backend/src/core/utils/fileio.py": 5783081738676692209, - "backend/src/api/routes/mappings.py": 7071183920129492341, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/crud.py": 805684028038896506, - "venv/lib/python3.13/site-packages/pydantic_core/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_inclusive.py": 1389676378885557519, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_reductions.py": 2816859231048302619, - "venv/lib/python3.13/site-packages/sqlalchemy/future/__init__.py": 17688444679736059779, - "backend/src/services/auth_service.py": 17642785219801916280, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/kbkdf.py": 1359143503575245224, - "venv/lib/python3.13/site-packages/requests/__version__.py": 6677532709758546816, - "venv/lib/python3.13/site-packages/cryptography/x509/name.py": 15338066443257445241, - "venv/lib/python3.13/site-packages/pygments/filter.py": 16846063190466267618, - "venv/lib/python3.13/site-packages/fastapi/requests.py": 14587947416691824903, - "venv/lib/python3.13/site-packages/numpy/testing/overrides.py": 11819432487830192978, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_shift.py": 9952689832827833247, - "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.py": 6142062990691556076, - "venv/lib/python3.13/site-packages/pandas/io/formats/__init__.py": 18140261896107421679, - "venv/lib/python3.13/site-packages/certifi/__main__.py": 13417658012431779061, - "venv/lib/python3.13/site-packages/pygments/lexers/math.py": 1647133236144311250, - "venv/lib/python3.13/site-packages/numpy/_core/shape_base.py": 3996519099530770308, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/env.py": 1254612137164491329, - "venv/lib/python3.13/site-packages/pandas/core/arrays/numeric.py": 17834058423729918687, - "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.cpython-313-x86_64-linux-gnu.so": 5284769758475394095, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_numpy.py": 222383250675686477, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_subplots.py": 4596089143375088346, - "venv/lib/python3.13/site-packages/httpx/_transports/wsgi.py": 11981127094916412794, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi": 13850254283540522444, - "venv/lib/python3.13/site-packages/jsonschema/cli.py": 9813914882090789748, - "venv/lib/python3.13/site-packages/h11/py.typed": 12358286306858197118, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_utils.py": 15197339054699289627, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq_py.py": 16054175754674285311, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/__init__.py": 2397613800206300456, - "venv/lib/python3.13/site-packages/pydantic_settings/__init__.py": 4374954204580208673, - "venv/lib/python3.13/site-packages/numpy/typing/mypy_plugin.py": 1306129775649200113, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_validators.py": 14178329676643700151, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_records.py": 18167578114159312327, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/array.py": 12658302948819867972, - "venv/lib/python3.13/site-packages/cryptography/x509/general_name.py": 4481582226129488535, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_conversion_utils.py": 1567199588622767277, - "venv/lib/python3.13/site-packages/pandas/errors/cow.py": 14381282473643980776, - "venv/lib/python3.13/site-packages/greenlet/TThreadStateDestroy.cpp": 13749797532421836328, - "venv/lib/python3.13/site-packages/pandas/tests/test_downstream.py": 5635510753354256507, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_pickle.py": 388513670243918273, - "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet_trash.py": 6191655521667576604, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_astype.py": 563689238486630866, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein_py.py": 11350239659973489646, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_sample.py": 4641567828238191927, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.py": 6348811074249398741, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/rapidfuzz/__init__.pyi": 13131938031654453415, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_parameter.py": 15173522343126004527, - "venv/lib/python3.13/site-packages/_pytest/_code/__init__.py": 6211607282116942091, - "venv/lib/python3.13/site-packages/pandas/_libs/testing.cpython-313-x86_64-linux-gnu.so": 16758333404587340103, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_numba.py": 1668304729617150075, - "venv/lib/python3.13/site-packages/attrs/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/common.py": 2075178842766962782, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/integration.py": 12670848015915724967, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_concatenate_chunks.py": 9693420220003544490, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_droplevel.py": 2071197388394658690, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_array.py": 11271807719833350332, - "venv/lib/python3.13/site-packages/websockets/sync/server.py": 7966978001578584901, - "venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py": 5076219228736706283, - "venv/lib/python3.13/site-packages/PIL/PalmImagePlugin.py": 13008868719182630239, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/METADATA": 15225661995655327539, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/align.py": 6078666505647454830, - "venv/lib/python3.13/site-packages/pygments/lexers/igor.py": 6506221273085723501, - "venv/lib/python3.13/site-packages/urllib3/util/connection.py": 15511685694589961094, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/METADATA": 15987199384245264478, - "venv/lib/python3.13/site-packages/numpy/fft/tests/test_helper.py": 5362180645056413158, - "venv/lib/python3.13/site-packages/PIL/_imagingtk.pyi": 18222325750818585549, - "venv/lib/python3.13/site-packages/httpcore/_synchronization.py": 10997080307461682786, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_interpolate.py": 6154072863387383113, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_bar.py": 7791068963246005463, - "venv/lib/python3.13/site-packages/pycparser/ply/ygen.py": 5520340773074632724, - "venv/lib/python3.13/site-packages/starlette/exceptions.py": 15978811612997200737, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_map.py": 32086858815359344, - "venv/lib/python3.13/site-packages/pygments/lexers/html.py": 17288963642862550116, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_finfo.py": 17174674209412768422, - "venv/lib/python3.13/site-packages/git/repo/__init__.py": 6173604284490891047, - "venv/lib/python3.13/site-packages/pygments/lexers/solidity.py": 5071425960622305629, - "frontend/src/lib/components/translate/__tests__/test_translation_preview.svelte.js": 15091004230843271151, - "frontend/src/components/Navbar.svelte": 12353588862181566761, - "frontend/build/_app/immutable/nodes/3.TrEd7Loa.js": 18117679530551903771, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/modules.pyi": 131293606732000587, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py": 5995058808539530576, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_hour.py": 13754646634123368797, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_observance.py": 18178414966262524534, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_musllinux.py": 2423350951094915747, - "frontend/build/_app/immutable/chunks/DOxI2NAm.js": 11236647372068635428, - "backend/src/core/auth/oauth.py": 9925908783612235042, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_old_ma.py": 18116460138221458026, - "venv/lib/python3.13/site-packages/rapidfuzz/_utils.py": 14374663810542207121, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_series.pyi": 7112486730197067087, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_equals.py": 9405821443172167066, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reflection.py": 1799734694597531652, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_setitem.py": 15731267129055947469, - "venv/lib/python3.13/site-packages/pygments/lexers/pony.py": 2720635393743617141, - "venv/lib/python3.13/site-packages/pandas/api/typing/__init__.py": 6438268275857060114, - "venv/lib/python3.13/site-packages/numpy/random/tests/__init__.py": 15130871412783076140, - "merge_kilo.py": 4282227344165325430, - "venv/lib/python3.13/site-packages/greenlet/greenlet_cpython_compat.hpp": 8396413256279803400, - "venv/lib/python3.13/site-packages/numpy/typing/tests/test_isfile.py": 14544060093268573418, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/_writer.py": 15053365792216907149, - "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.pyi": 12144531601504755948, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.pyi": 13208953770241664384, - "venv/lib/python3.13/site-packages/pandas/core/indexes/interval.py": 17965911911189276907, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_stack_unstack.py": 3737891757817696405, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_converter.py": 7709581879446228856, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_partial_indexing.py": 4132091154215910029, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_shift.py": 14026124784027171225, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args.py": 5821040955806257450, - "venv/lib/python3.13/site-packages/starlette/middleware/authentication.py": 15211131264936212938, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/einsumfunc.pyi": 200771351328522379, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.py": 5221550291012182443, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.cpython-313-x86_64-linux-gnu.so": 16927263232848238698, - "venv/lib/python3.13/site-packages/fastapi/middleware/__init__.py": 5289703478893407762, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polynomial.py": 777287409151500309, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/macos.py": 17442393555211213147, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/METADATA": 13695483537390128881, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_strings.py": 17398806130402098912, - "venv/lib/python3.13/site-packages/numpy/f2py/__init__.py": 7470157159992827389, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py": 7638945613579396718, - "venv/lib/python3.13/site-packages/pip/_internal/models/direct_url.py": 4619204214785230341, - "venv/lib/python3.13/site-packages/pandas/compat/pickle_compat.py": 11073483984394766474, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_astype.py": 11386658862732393123, - "venv/lib/python3.13/site-packages/websockets/asyncio/router.py": 15039562376314233669, - "venv/lib/python3.13/site-packages/numpy/random/lib/libnpyrandom.a": 1802449692091040396, - "venv/lib/python3.13/site-packages/secretstorage/defines.py": 9590959702075554048, - "venv/lib/python3.13/site-packages/passlib/handlers/cisco.py": 15004707325761459775, - "venv/lib/python3.13/site-packages/pandas/plotting/_core.py": 6766059152026241747, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_attr_equal.py": 5612259834202470413, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_construction.py": 6564829381134214401, - "venv/lib/python3.13/site-packages/httpx/_urls.py": 4219389176352582046, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hmac.py": 18132740541139891824, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nested_sequence.pyi": 10985533017688918283, - "venv/lib/python3.13/site-packages/pandas/testing.py": 18352975429944961593, - "venv/lib/python3.13/site-packages/urllib3/util/ssl_.py": 11720964010284185952, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/RECORD": 16874983218652404911, - "venv/lib/python3.13/site-packages/numpy/random/c_distributions.pxd": 16574161095720077595, - "venv/lib/python3.13/site-packages/pygments/styles/gh_dark.py": 7717523746712391809, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period.py": 9715843209581883915, - "venv/lib/python3.13/site-packages/pygments/lexers/_vim_builtins.py": 6842381248727719900, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/__init__.py": 6758781485387665020, - "venv/lib/python3.13/site-packages/fastapi/openapi/docs.py": 16735218221639179927, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_value_counts.py": 9352540710376306520, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_sorting.py": 10523740688498427929, - "venv/lib/python3.13/site-packages/charset_normalizer/cd.py": 8687667152597941021, - "frontend/src/routes/settings/__tests__/settings_page.integration.test.js": 13008989176397099996, - "frontend/src/components/RepositoryDashboardGrid.svelte": 17100594220040865069, - "frontend/build/_app/immutable/chunks/BURGrVTR.js": 9373329903010633175, - "backend/src/core/superset_client/_dashboards_list.py": 16574697057051064609, - "backend/src/core/plugin_base.py": 11649432288706102843, - "backend/src/models/config.py": 7631082065091961320, - "backend/src/services/reports/type_profiles.py": 8365014439236739390, - "venv/lib/python3.13/site-packages/pygments/lexers/q.py": 15426466074881683794, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/jwt.py": 9642615960197748105, - "venv/lib/python3.13/site-packages/idna/idnadata.py": 200840860629525970, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arithmetic.pyi": 10265823318697442495, - "venv/lib/python3.13/site-packages/httpx/_status_codes.py": 1758440600462491839, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/comparisons.pyi": 9338084426044982038, - "venv/lib/python3.13/site-packages/jaraco/context/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/_pytest/warning_types.py": 10695097290648045427, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate.py": 11205714006000849926, - "venv/lib/python3.13/site-packages/keyring/compat/__init__.py": 7578020229796085753, - "venv/lib/python3.13/site-packages/numpy/dtypes.py": 14128421398870718692, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/common.py": 16314033415206173235, - "venv/lib/python3.13/site-packages/pandas/io/spss.py": 864838144958947906, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/random.pyi": 1808392097712315455, - "venv/lib/python3.13/site-packages/anyio/to_interpreter.py": 8369454842687342401, - "frontend/src/lib/components/reports/ReportDetailPanel.svelte": 16014663571033060148, - "venv/lib/python3.13/site-packages/pandas/_libs/internals.cpython-313-x86_64-linux-gnu.so": 2008633361686956377, - "backend/src/services/clean_release/repositories/publication_repository.py": 11208425895213508165, - "backend/src/services/reports/__init__.py": 11818812865847046400, - "venv/lib/python3.13/site-packages/httpx/_content.py": 1301854837767395687, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/RECORD": 1051307407005554409, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_zips.py": 5184894018177148648, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/lower_f2py_fortran.f90": 13315882216322269383, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/wrappers.py": 315409799569602977, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/bitwise_ops.pyi": 15598239133239520456, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_analytics.py": 4510712334676848202, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_ctors.py": 631744056514965940, - "venv/lib/python3.13/site-packages/pandas/core/interchange/column.py": 9498044216416177656, - "venv/lib/python3.13/site-packages/pydantic/_internal/_signature.py": 17075003201648027250, - "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.pyi": 10077265709692546934, - "venv/lib/python3.13/site-packages/dotenv/parser.py": 17651523026132242005, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24662.f90": 4241310200087270990, - "venv/lib/python3.13/site-packages/pandas/_libs/indexing.cpython-313-x86_64-linux-gnu.so": 13188040757764159731, - "venv/lib/python3.13/site-packages/pandas/core/computation/parsing.py": 3084943521520476999, - "venv/lib/python3.13/site-packages/pandas/core/indexers/__init__.py": 1756397319068017890, - "venv/lib/python3.13/site-packages/pydantic/v1/parse.py": 5875950480538559709, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/validation": 13992741783834278677, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/encoder.py": 8912119133548489544, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/api.py": 12339139862411341381, - "venv/lib/python3.13/site-packages/pandas/core/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/models.py": 3567900447243180991, - "venv/lib/python3.13/site-packages/git/index/util.py": 5022401275553420722, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/METADATA": 12747626683405487434, - "venv/lib/python3.13/site-packages/numpy/lib/introspect.py": 5706729538356302335, - "venv/lib/python3.13/site-packages/uvicorn/__init__.py": 11517239807926185665, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/RECORD": 9964194961227523130, - "venv/lib/python3.13/site-packages/pygments/lexers/usd.py": 934820244673741695, - "venv/lib/python3.13/site-packages/numpy/lib/scimath.py": 7616377937400918724, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.cpython-313-x86_64-linux-gnu.so": 11019871270094058038, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/hotp.py": 18325640697829098192, - "venv/lib/python3.13/site-packages/charset_normalizer/md.py": 2029760700380476138, - "frontend/src/lib/stores.js": 8062876368729352764, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/size/foo.f90": 18067977329435720684, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/checks.pyx": 13187505236986855177, - "frontend/src/components/TaskList.svelte": 13727273029173605651, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/dependency.py": 7300594038182867460, - "venv/lib/python3.13/site-packages/PIL/ImageTransform.py": 7825940907094338808, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/containers.py": 5969831151685016772, - "venv/lib/python3.13/site-packages/urllib3/fields.py": 9503202008440141421, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_find_replace.py": 262184394614748386, - "venv/lib/python3.13/site-packages/urllib3/http2/probe.py": 4069929463671970243, - "venv/lib/python3.13/site-packages/pandas/tests/extension/list/__init__.py": 10072943986583771248, - "venv/lib/python3.13/site-packages/git/index/typ.py": 11661540784850363024, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/orm.py": 7554417559838785150, - "venv/lib/python3.13/site-packages/pydantic/deprecated/decorator.py": 8609500261161560793, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_datetime_index.py": 15448494560143263150, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_conversion.py": 6205112277291935767, - "venv/lib/python3.13/site-packages/pandas/tests/test_errors.py": 10116716310156374885, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/conftest.py": 13388202260795998613, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_scalar_compat.py": 10534925655070162459, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_constructors.py": 7466113171256856591, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/WHEEL": 7795541085444298627, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/style.py": 15214295655795348821, - "venv/lib/python3.13/site-packages/pandas/io/iceberg.py": 13137128885014736233, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/statreload.py": 5018382301336802445, - "venv/lib/python3.13/site-packages/pygments/lexers/resource.py": 8057351545636349791, - "frontend/build/_app/immutable/nodes/6.CQhNV4xO.js": 10554106729411192187, - "backend/src/core/scheduler.py": 2472649939852521889, - "backend/src/services/clean_release/__tests__/test_policy_engine.py": 9953185933720550640, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_pick.py": 15760492005471315814, - "frontend/build/_app/immutable/chunks/BrvzRogk.js": 16712439889682616806, - "venv/lib/python3.13/site-packages/psycopg2/__init__.py": 15798412693009721199, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh17797.f90": 17319074662077763768, - "venv/lib/python3.13/site-packages/pydantic/_migration.py": 17982816821930887921, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/asn1.pyi": 16048852379621461316, - "venv/lib/python3.13/site-packages/referencing/jsonschema.py": 14596871117484757502, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro_py.py": 11991492232381887787, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_indexing.py": 14717641281147152159, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/index.py": 15694995431926521471, - "venv/lib/python3.13/site-packages/annotated_doc/__init__.py": 6834198874252778425, - "venv/lib/python3.13/site-packages/pandas/tests/test_flags.py": 411845322592830527, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexing.py": 5910217172351020811, - "venv/lib/python3.13/site-packages/PIL/GbrImagePlugin.py": 7681313333602257802, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/RECORD": 17341460504914308184, - "backend/src/core/superset_client/__init__.py": 6869599313455380203, - "backend/src/api/__init__.py": 5287505122015708040, - "backend/src/services/git/_branch.py": 7552472942680784566, - "frontend/build/_app/immutable/chunks/2dOUpm6k.js": 11811637653151731489, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_header.py": 3564391095263545879, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_rewrite_warning.py": 4895919872954538758, - "backend/src/api/routes/dashboards/_schemas.py": 10686942001147126057, - "frontend/src/routes/dashboards/[id]/components/DashboardHeader.svelte": 5572977872383619871, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.BSD": 3858254215169937333, - "venv/lib/python3.13/site-packages/httpx/_api.py": 6697370469329778523, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py": 11139432164346742421, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_regression.py": 16266600037241221681, - "venv/lib/python3.13/site-packages/pandas/core/arrays/masked.py": 13977195666947626206, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/cache.py": 15037055977924167732, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_pickle.py": 16196792378104396231, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.pyi": 10947021928755741366, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_replace.py": 8166203537440976347, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py": 3515239440812977025, - "venv/lib/python3.13/site-packages/pyasn1/codec/native/encoder.py": 120921270972591880, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_astype.py": 4178231007649608531, - "frontend/src/lib/api/reports.js": 2328857814720054000, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.pyi": 12925006572914814268, - "venv/lib/python3.13/site-packages/pip/_internal/cli/spinners.py": 7664056305019151182, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_openid.py": 8708980645667847439, - "venv/lib/python3.13/site-packages/authlib/oauth1/__init__.py": 3692257288065292607, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/types.py": 1343652083039949282, - "venv/lib/python3.13/site-packages/pydantic/v1/class_validators.py": 6471688474853407718, - "venv/lib/python3.13/site-packages/httpx/__init__.py": 6383055693166097719, - "venv/lib/python3.13/site-packages/_pytest/config/exceptions.py": 3199245296954654462, - "venv/lib/python3.13/site-packages/fastapi/dependencies/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pillow.libs/libbrotlicommon-c55a5f7a.so.1.2.0": 2635192149914499986, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arrow_interface.py": 5231449831607036031, - "venv/lib/python3.13/site-packages/charset_normalizer/legacy.py": 1706599349563920562, - "frontend/src/lib/auth/__tests__/permissions.test.js": 13555349356008297952, - "venv/lib/python3.13/site-packages/_pytest/reports.py": 13930709338396699351, - "venv/lib/python3.13/site-packages/pycparser/c_lexer.py": 10964496587169353564, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.cpython-313-x86_64-linux-gnu.so": 12776951671822017500, - "backend/src/models/__tests__/test_models.py": 15975002719327784362, - "backend/src/services/git/_remote_providers.py": 12661659133898682960, - "venv/lib/python3.13/site-packages/psycopg2/sql.py": 18041240927130590808, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/value_attrspec/gh21665.f90": 11945588837236968550, - "venv/lib/python3.13/site-packages/passlib/handlers/mysql.py": 11165762635383041460, - "venv/lib/python3.13/site-packages/pandas/io/feather_format.py": 13242733585787888812, - "venv/lib/python3.13/site-packages/pygments/lexers/gdscript.py": 12775803139854052838, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_deprecations.py": 16124509063246256847, - "venv/lib/python3.13/site-packages/yaml/cyaml.py": 9157893742065554693, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/assertion_client.py": 4261295109880943573, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/table.py": 16856253349556498073, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd_module.py": 7122050193390298178, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml_dtypes.py": 18293138335799308439, - "venv/lib/python3.13/site-packages/pygments/lexers/business.py": 10410439210806697214, - "venv/lib/python3.13/site-packages/pip/_internal/__init__.py": 7060345458485901709, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/unicode_comment.f90": 11326797716215117266, - "venv/bin/pygmentize": 2166491742035772877, - "venv/lib/python3.13/site-packages/pydantic/generics.py": 15019381413806459410, - "venv/lib/python3.13/site-packages/pandas/_libs/properties.pyi": 9268470215750269699, - "venv/lib/python3.13/site-packages/keyring/backend_complete.bash": 16589214602970214174, - "venv/lib/python3.13/site-packages/keyring/backends/fail.py": 5792496235026678021, - "venv/lib/python3.13/site-packages/dotenv/variables.py": 5438726379749803751, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isetitem.py": 14895552405828068177, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/__init__.py": 16434403703599680307, - "venv/lib/python3.13/site-packages/pygments/lexers/maxima.py": 11824576541091274641, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated": 11193150813314469394, - "venv/lib/python3.13/site-packages/pygments/lexers/smv.py": 8253688483040957894, - "venv/lib/python3.13/site-packages/pandas/_libs/reshape.pyi": 88721900806893992, - "frontend/build/_app/immutable/chunks/BG4NfqIk.js": 6096001408084116703, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/json.py": 14738429697319670035, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_collections.py": 14307204206312829238, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_where.py": 14167991754625133668, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/shared_docs.py": 2173578869755325502, - "frontend/build/_app/immutable/chunks/BoOsFBJ0.js": 11874060117929105318, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval.py": 18040371224712437126, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numerictypes.py": 5719374287454872555, - "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.py": 7365943844063702766, - "venv/lib/python3.13/site-packages/numpy/_core/_internal.pyi": 6840742333427164920, - "venv/lib/python3.13/site-packages/gitdb/test/test_pack.py": 7518388297247535189, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/rec.pyi": 14615751247243515028, - "venv/lib/python3.13/site-packages/pandas/core/computation/expressions.py": 10684284531040203747, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py": 12632271159842988561, - "backend/src/core/middleware/trace.py": 13093835356377196397, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_fillna.py": 8802093795040069423, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py": 7111764828004587758, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/ops.py": 13451323660386712332, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.c": 9318655518998644962, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_null_file.py": 6885565068723353414, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.pyi": 3595910367556664837, - "venv/lib/python3.13/site-packages/dotenv/ipython.py": 15904966766060247908, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_indexing_slow.py": 12558235638488427077, - "venv/lib/python3.13/site-packages/pip/_internal/operations/install/__init__.py": 10456333337205488584, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connectionpool.py": 10453724680327835821, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/timeseries.py": 17641722582848932332, - "venv/lib/python3.13/site-packages/pandas/io/pytables.py": 17482934215513855928, - "venv/lib/python3.13/site-packages/pygments/lexers/pawn.py": 18127303119414402594, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_clip.py": 13498704233745189154, - "venv/lib/python3.13/site-packages/pandas/core/indexes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic/v1/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/__init__.py": 1398617034174236358, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_conversion.pyi": 11580106670226366710, - "venv/lib/python3.13/site-packages/pygments/styles/paraiso_dark.py": 3976779833379189837, - "frontend/src/routes/profile/__tests__/fixtures/profile.fixtures.js": 2726190101012864209, - "backend/src/services/dataset_review/orchestrator.py": 8780497744077644822, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/base.cpython-313-x86_64-linux-gnu.so": 8335798324308957127, - "venv/lib/python3.13/site-packages/httpcore/_sync/socks_proxy.py": 4418971149260853240, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/unrolled.py": 12651425412640834530, - "venv/lib/python3.13/site-packages/numpy/__config__.py": 15794735729525094396, - "venv/lib/python3.13/site-packages/apscheduler/triggers/base.py": 1170652622104554892, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.py": 15426828792217499138, - "venv/lib/python3.13/site-packages/pandas/tests/libs/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic/class_validators.py": 4643597082531287188, - "venv/lib/python3.13/site-packages/pip/_internal/models/scheme.py": 4101322315594430874, - "venv/lib/python3.13/site-packages/fastapi/_compat/main.py": 13016492818231995760, - "venv/lib/python3.13/site-packages/websockets/client.py": 5229500079651465772, - "venv/lib/python3.13/site-packages/fastapi/middleware/wsgi.py": 3706578020491442104, - "venv/lib/python3.13/site-packages/more_itertools/__init__.pyi": 16479506520351014608, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/ec_key.py": 5961540064444749833, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/util.py": 387985965709546512, - "venv/lib/python3.13/site-packages/pandas/core/reshape/merge.py": 300205528811693975, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/revocation.py": 5718403706971163960, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_parser.py": 8145921552236189367, - "venv/lib/python3.13/site-packages/pydantic/warnings.py": 5142352784799639092, - "venv/lib/python3.13/site-packages/pygments/lexers/arturo.py": 4565524340706235725, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_ndarray_backed.py": 10097812802469682233, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.pyi": 14233146512272397376, - "frontend/src/lib/components/dataset-review/CompiledSQLPreview.svelte": 14788599185006992431, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/format": 12697928260201325593, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_series.py": 17126297747681689408, - "backend/src/plugins/translate/executor.py": 16483493819862607738, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/LICENSE": 6774760218010069963, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/response.py": 15004896908941411698, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_skew_kurt.py": 15225601437354712351, - "backend/src/api/routes/__tests__/test_datasets.py": 14530822388357135497, - "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.py": 15989335816452913951, - "venv/lib/python3.13/site-packages/numpy/core/umath.py": 4476812261188108155, - "venv/lib/python3.13/site-packages/httpx/_decoders.py": 14478643473006178492, - "venv/lib/python3.13/site-packages/starlette/routing.py": 16063812008141874150, - "venv/lib/python3.13/site-packages/pydantic/_internal/_model_construction.py": 694471028676746110, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/passlib/handlers/windows.py": 8969739861295277439, - "venv/lib/python3.13/site-packages/PIL/_tkinter_finder.py": 14045496501810535432, - "venv/lib/python3.13/site-packages/pygments/lexers/forth.py": 17623561228839319663, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.pyf": 4660465266594480405, - "frontend/src/components/MappingTable.svelte": 3101497452073042168, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x448.py": 3960540415600165213, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_indexing.py": 8943385096811168079, - "frontend/build/_app/immutable/chunks/CbSNjLmH.js": 12377182002440958587, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/models.py": 15928924478695638423, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/__init__.py": 96341051676048910, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/decoder.py": 12653470212935324322, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_processors.py": 16557117595879208702, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/wsproto_impl.py": 3072714633272661847, - "venv/lib/python3.13/site-packages/yaml/constructor.py": 8731384179016992045, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pyodbc.py": 8175468565413744516, - "venv/lib/python3.13/site-packages/attr/validators.py": 4453080662115911998, - "venv/lib/python3.13/site-packages/jose/jws.py": 11592462993417182096, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/testing.pyi": 9317922890783549326, - "venv/lib/python3.13/site-packages/pandas/_libs/sparse.cpython-313-x86_64-linux-gnu.so": 2051484129454294720, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.pyi": 15789956857032062129, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_join.py": 5982692266872978444, - "venv/lib/python3.13/site-packages/pygments/styles/paraiso_light.py": 15941591873059208999, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_subclass.py": 12453252499544981915, - "frontend/src/lib/stores/__tests__/assistantChat.test.js": 11822882112255142537, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_pytables_missing.py": 12154582133989896069, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/groupby.py": 14661033005768429774, - "venv/lib/python3.13/site-packages/pydantic/_internal/_known_annotated_metadata.py": 9308620143218174987, - "venv/lib/python3.13/site-packages/pandas/_libs/join.cpython-313-x86_64-linux-gnu.so": 2947947484481830203, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_halfyear.py": 9376272342364384767, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/foo.f90": 14491312943942432024, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/__init__.py": 7062045842767697645, - "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pyi": 14150710436003340771, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/use_data.f90": 14624074967635860696, - "venv/lib/python3.13/site-packages/starlette/websockets.py": 13834731343216764204, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.py": 652332294655046106, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/__init__.py": 11850945878149145990, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh22648.pyf": 15094859289098837494, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/intranges.py": 12536174834761591006, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/events.py": 10224214051422678828, - "venv/lib/python3.13/site-packages/PIL/ContainerIO.py": 7551929914241379005, - "venv/lib/python3.13/site-packages/pygments/lexers/asm.py": 13861952972248758785, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_at.py": 1029744344133153888, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_hashtable.py": 2620207240930665390, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.pyi": 13110894861187422750, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth1_session.py": 1911069558956893789, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_truncate.py": 9623613657871046837, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqldb.py": 2138006249556140141, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_reductions.py": 3575617237516305456, - "frontend/src/components/tools/ConnectionList.svelte": 15981533028345710336, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh2848.f90": 5799106109999071412, - "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/test_decimal.py": 17811784255174057561, - "backend/src/api/routes/git/__init__.py": 9989336734947065016, - "venv/lib/python3.13/site-packages/httpcore/_backends/auto.py": 10488908050307887802, - "venv/lib/python3.13/site-packages/greenlet/greenlet_allocator.hpp": 11041111583319491309, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/missing.py": 11241703105777668183, - "venv/lib/python3.13/site-packages/pygments/lexers/lilypond.py": 1088755646568511197, - "venv/lib/python3.13/site-packages/pandas/tests/test_expressions.py": 16378925536309828288, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_repeat.py": 18394463352314692642, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/LICENSE": 12064892310002036996, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/compiler.py": 13924519162400330589, - "venv/lib/python3.13/site-packages/_pytest/hookspec.py": 387790426108810884, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.pyi": 16372699564794732463, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/auth.py": 912521029864094489, - "venv/lib/python3.13/site-packages/authlib/oauth2/base.py": 12948318231066662045, - "venv/lib/python3.13/site-packages/websockets/speedups.pyi": 10322382420215791091, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein_py.py": 1933257612725836484, - "venv/lib/python3.13/site-packages/pip/_internal/utils/filesystem.py": 7716285033598020316, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/WHEEL": 7127684561765977531, - "venv/lib/python3.13/site-packages/dotenv/py.typed": 9796674040111366709, - "venv/lib/python3.13/site-packages/passlib/registry.py": 5050675058011850564, - "venv/lib/python3.13/site-packages/pandas/core/interchange/buffer.py": 12961802206383700922, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/revocation.py": 16178499278312777474, - "venv/lib/python3.13/site-packages/fastapi/security/oauth2.py": 1925394177931547776, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_index_as_string.py": 292438690259156869, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/attr/_make.py": 2348458829123990579, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py": 16386268386376675175, - "venv/lib/python3.13/site-packages/pandas/tests/extension/json/__init__.py": 7877061921210840955, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/RECORD": 7320381591791379627, - "venv/lib/python3.13/site-packages/pandas/io/_util.py": 13608922148814398500, - "venv/lib/python3.13/site-packages/rsa/transform.py": 12261738253972114335, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py": 9931394693284776391, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/utils.py": 9959199071030996679, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/types.py": 12298292195929126306, - "venv/lib/python3.13/site-packages/_pytest/__init__.py": 3769333500910148571, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/content": 16257335642847993866, - "venv/lib/python3.13/site-packages/dateutil/utils.py": 7067026915090881998, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/basereload.py": 14479857245734482609, - "venv/lib/python3.13/site-packages/PIL/FliImagePlugin.py": 13364963053197422923, - "venv/lib/python3.13/site-packages/rsa/core.py": 13203947234024924497, - "venv/lib/python3.13/site-packages/packaging/metadata.py": 9168255069163141470, - "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.pyi": 3111521453255595221, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/authorization_server.py": 2491654903543600678, - "venv/lib/python3.13/site-packages/_pytest/mark/structures.py": 12237146603608917650, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/horizontal_shard.py": 17615732108308858890, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/apps.py": 10758062668702044519, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_constructors.py": 11786151805350138436, - "frontend/src/lib/components/layout/__tests__/sidebarNavigation.test.js": 9823973527644599938, - "frontend/src/pages/Dashboard.svelte": 3653104499351855122, - "venv/lib/python3.13/site-packages/click/utils.py": 13475604420010022458, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2cmap.py": 5863341586534531594, - "venv/lib/python3.13/site-packages/anyio/_core/_fileio.py": 4507100568401692983, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/RECORD": 1689342214025776630, - "venv/lib/python3.13/site-packages/numpy/tests/test_public_api.py": 11741922758622705449, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_hist_box_by.py": 16197297231683144299, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.py": 5997687240967250164, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/RECORD": 12598309574903741388, - "merge_spec.py": 5285945727297582371, - "backend/src/services/git/_url.py": 15729080796864187707, - "frontend/build/_app/immutable/nodes/25.Dz6lg2iG.js": 9988963929265574361, - "backend/alembic/README": 17625003422504608299, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_assign.py": 2647902466315482406, - "venv/lib/python3.13/site-packages/pandas/tests/config/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/ooc.py": 14540210212631538007, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_complex.py": 11732242536199439023, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.cpython-313-x86_64-linux-gnu.so": 12654188486128938170, - "venv/lib/python3.13/site-packages/httpx/_config.py": 8800538050293498928, - "venv/lib/python3.13/site-packages/numpy/random/__init__.pyi": 13833571330152615376, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_indexing.py": 17106010337164216722, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reserved_words.py": 8022121780993497971, - "venv/bin/numpy-config": 17104619511899110252, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_copy.py": 11335108864615756235, - "backend/src/__init__.py": 11250920420388310655, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reset_index.py": 14490875921264161445, - "venv/lib/python3.13/site-packages/passlib/exc.py": 15559705487889657091, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_functions.py": 16376319335326812580, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polybase.pyi": 3404315099794022355, - "venv/lib/python3.13/site-packages/greenlet/CObjects.cpp": 3454797518210223926, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/WHEEL": 1598805043198466373, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py": 9627328185578406121, - "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.py": 4940170059946018356, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/RECORD": 5169907629260560698, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unstack.py": 11834917665446437163, - "venv/lib/python3.13/site-packages/starlette/_exception_handler.py": 9892172656847200124, - "venv/lib/python3.13/site-packages/PIL/PpmImagePlugin.py": 7107324729714470853, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_complex.py": 12963449957363121481, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/applicator": 6870991911243684132, - "venv/lib/python3.13/site-packages/authlib/consts.py": 14226604841596736870, - "venv/lib/python3.13/site-packages/numpy/random/_common.pxd": 3402284125943848851, - "venv/lib/python3.13/site-packages/requests/compat.py": 5836590172733661320, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/base.py": 10200916670889920659, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/parse.py": 2427019397934437210, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_real.py": 5283415082530228716, - "venv/lib/python3.13/site-packages/pycparser/ply/lex.py": 3393278208465358140, - "venv/lib/python3.13/site-packages/pygments/lexers/mime.py": 7768464764849718825, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/WHEEL": 691462085217888236, - "venv/lib/python3.13/site-packages/pygments/styles/lilypond.py": 12486578704132023691, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_contains.py": 17630310725250553247, - "frontend/src/lib/components/reports/ReportCard.svelte": 15186215132458187779, - "venv/lib/python3.13/site-packages/pygments/formatters/img.py": 18230180171537565208, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_free.f90": 10939657740734987454, - "frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte": 10137468959835535437, - "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js": 17118608261683412409, - "venv/lib/python3.13/site-packages/pydantic/deprecated/copy_internals.py": 1245428320513062166, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.py": 1341849861184131135, - "venv/lib/python3.13/site-packages/dotenv/__init__.py": 15464030923609356017, - "venv/lib/python3.13/site-packages/numpy/lib/_version.py": 15294708923334674060, - "venv/lib/python3.13/site-packages/pandas/core/series.py": 11618272682977710696, - "venv/lib/python3.13/site-packages/pandas/io/formats/info.py": 215303194287834907, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/METADATA": 13428590545328703138, + "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.pyi": 6286629088867436848, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_insert.py": 6970975909565157982, "venv/lib/python3.13/site-packages/PIL/GifImagePlugin.py": 16073125611077200693, - "venv/lib/python3.13/site-packages/pygments/styles/__init__.py": 18383248976818885498, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_dml_constructors.py": 4497038002189188311, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/npyio.pyi": 15811156298637131744, - "venv/lib/python3.13/site-packages/attrs/__init__.py": 9502968706258875910, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/METADATA": 3618776395694990076, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_datetimelike.py": 17304541521108700317, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/WHEEL": 9131187629260560775, - "venv/lib/python3.13/site-packages/PIL/SunImagePlugin.py": 7661622930404688367, - "venv/lib/python3.13/site-packages/pygments/__main__.py": 13200500591994479045, - "venv/lib/python3.13/site-packages/pygments/lexers/bdd.py": 5389493859905397060, - "venv/lib/python3.13/site-packages/PIL/ImageFilter.py": 10974179993355630141, - "frontend/src/routes/translate/dictionaries/[id]/+page.svelte": 14484918248250087235, - "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.py": 7029935879540617057, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_conversion.py": 7507083194555424923, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/sqltypes.py": 2952996080192295188, - "venv/lib/python3.13/site-packages/pytest/py.typed": 15130871412783076140, - "venv/bin/pyrsa-priv2pub": 16766314338551408621, - "venv/bin/pytest": 2593802493707545818, - "frontend/build/_app/immutable/nodes/20.hRBN7DT6.js": 13368106004770721520, - "backend/src/models/dataset_review_pkg/_mapping_models.py": 6232602161916984422, - "venv/lib/python3.13/site-packages/numpy/testing/__init__.pyi": 8126257788928282891, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test__iotools.py": 6260912730310453037, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex.tpl": 8820045022551446255, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_cython_aggregations.py": 10818895573637904764, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_kurt.py": 2898715045429898166, - "backend/alembic/versions/543d43d752b8_migrate_old_dictionary_entries_and_.py": 10988663686444687907, - "venv/lib/python3.13/site-packages/rsa/pem.py": 3118524021576082054, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/validator_creation.py": 2745563768861102999, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-1.csv": 2202360898679162333, - "frontend/src/lib/components/dataset-review/SourceIntakePanel.svelte": 10994864579319870671, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_array.f90": 9711940829820746513, - "venv/lib/python3.13/site-packages/authlib/integrations/django_client/apps.py": 14151733532064278336, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/models.py": 5656065513234782067, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__main__.py": 6480933938145689964, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_real.f90": 9097998998863007705, - "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.pyi": 6072607880689125113, - "venv/lib/python3.13/site-packages/pandas/tests/extension/conftest.py": 16886862391161243448, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/asyncio.py": 4200928269459447593, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/WHEEL": 7414522633964952515, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_policy.py": 13856626567322091253, - "venv/lib/python3.13/site-packages/numpy/version.py": 1497063540123613501, - "scripts/build_offline_docker_bundle.sh": 18334040481218597997, - "venv/lib/python3.13/site-packages/pydantic/plugin/_schema_validator.py": 857640644840976862, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/conftest.py": 12368472226536104037, - "venv/lib/python3.13/site-packages/anyio/abc/_tasks.py": 16839170934610571514, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_numba.py": 203614377324678234, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_info.py": 15151894356374015812, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_crackfortran.py": 10278720705326601943, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/__init__.py": 14522970260621605208, - "venv/lib/python3.13/site-packages/pandas/tests/resample/conftest.py": 1643522609234951699, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.cpython-313-x86_64-linux-gnu.so": 6157288238855118017, - "venv/lib/python3.13/site-packages/_pytest/helpconfig.py": 3033477806668470951, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/util.py": 1520899420719605380, - "venv/lib/python3.13/site-packages/dateutil/_common.py": 12492546559232079071, - "venv/lib/python3.13/site-packages/pip/_vendor/__init__.py": 11734104142988812647, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/scanner.py": 5640381644804445932, - "venv/lib/python3.13/site-packages/starlette/convertors.py": 17839747154274354605, - "venv/lib/python3.13/site-packages/numpy/core/_utils.py": 14108063453670701289, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/kind/foo.f90": 1367964042730902899, - "venv/lib/python3.13/site-packages/pandas/_config/display.py": 433650013567734821, - "venv/lib/python3.13/site-packages/PIL/ExifTags.py": 6869879826623916830, - "venv/lib/python3.13/site-packages/pycparser/yacctab.py": 18018314512916353707, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/use_modules.f90": 3913612933340746297, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/markers.py": 5247208668583898949, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/__init__.py": 15644105878811738516, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/WHEEL": 11630459739183915695, - "frontend/src/components/git/CommitHistory.svelte": 15861316872235672161, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/AB.inc": 10441778083831997423, - "venv/lib/python3.13/site-packages/_pytest/stepwise.py": 3527630676691417317, - "venv/lib/python3.13/site-packages/pygments/lexers/ul4.py": 9580182474451220952, - "backend/src/schemas/profile.py": 7072251247808762582, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/ddl.py": 5006132143435653867, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_multiindex.py": 13723677252432338937, - "backend/src/core/task_manager/task_logger.py": 12820629458199400734, - "backend/src/plugins/translate/__tests__/test_inline_correction.py": 3246602656771855554, - "backend/src/plugins/translate/plugin.py": 9225230324052126034, - "venv/lib/python3.13/site-packages/attrs/__init__.pyi": 14353885498883901855, - "venv/lib/python3.13/site-packages/numpy/core/_internal.py": 14816989375912210674, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_scrypt.py": 8106466515670064013, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/__init__.py": 7526309124863950484, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_convert_dtypes.py": 13612212066710266457, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_extint128.py": 11074657869101074458, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/RECORD": 12733654377842895588, - "frontend/src/lib/auth/store.ts": 14669655798912377598, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/console.py": 10217327186846649646, - "venv/lib/python3.13/site-packages/cryptography/__init__.py": 641702726372647770, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/RECORD": 3649834117451110628, - "venv/lib/python3.13/site-packages/pandas/core/arrays/interval.py": 16281221279220043867, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_interval.py": 2911379732293827726, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/RECORD": 10036839994791510159, - "venv/lib/python3.13/site-packages/websockets/asyncio/messages.py": 6848925149930551457, - "venv/lib/python3.13/site-packages/numpy/_core/memmap.pyi": 14124415711399771715, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_kwargs.py": 10173906126211473463, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy.libs/libquadmath-96973f99-934c22de.so.0.0.0": 5098713761946118139, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/modules.pyi": 14176187662591164602, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_cte.py": 9274103293201050794, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/__init__.py": 12290228927283601067, - "venv/lib/python3.13/site-packages/pygments/lexers/felix.py": 2334148799642808111, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/base.py": 15872835330892203433, - "frontend/build/_app/immutable/nodes/13.Cuwx6oG8.js": 10896568729237690384, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_year.py": 17441853503700184299, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_invalid.py": 2762344062554575805, - "venv/lib/python3.13/site-packages/numpy/_core/__init__.py": 14951958843029076911, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_value_counts.py": 13270420637302622174, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_resolution.py": 18198858926948468112, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/__init__.py": 15130871412783076140, - "backend/src/plugins/translate/scheduler.py": 1593554077252050680, - "venv/lib/python3.13/site-packages/passlib/hash.py": 5238710192614935875, - "venv/lib/python3.13/site-packages/pandas/io/formats/css.py": 11288129826285529457, - "venv/lib/python3.13/site-packages/PIL/ImageShow.py": 11437096951764124817, - "venv/lib/python3.13/site-packages/pandas/tests/extension/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nditer.py": 8341339612899904236, - "venv/lib/python3.13/site-packages/numpy/_core/_methods.py": 16805668769357938792, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/LICENSE": 12688029424398428498, - "venv/lib/python3.13/site-packages/anyio/streams/stapled.py": 9733127303038397237, - "venv/lib/python3.13/site-packages/websockets/asyncio/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_constructors.py": 6727209833819502644, - "backend/src/api/routes/assistant/_admin_routes.py": 3380446401429936332, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/WHEEL": 6514509858522675228, - "venv/lib/python3.13/site-packages/_pytest/assertion/rewrite.py": 2425990712758915831, - "venv/lib/python3.13/site-packages/apscheduler/triggers/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_textreader.py": 388955620768371652, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_chebyshev.py": 8054411443686791426, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_delete.py": 3485374671877063217, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/aws.py": 1364297790170663941, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/padding.py": 13763154154862871596, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo77.f": 4046503346951676582, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_asfreq.py": 6162659555626011770, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/issue232.py": 6498198296113395367, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_all_methods.py": 18227706645906325527, - "venv/lib/python3.13/site-packages/pygments/formatters/irc.py": 2133711525226123259, - "venv/lib/python3.13/site-packages/pygments/__init__.py": 14493583771211110135, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/INSTALLER": 17282701611721059870, - "frontend/src/lib/stores/translationRun.js": 1541511355007861201, - "frontend/src/lib/components/dataset-review/ExecutionMappingReview.svelte": 11412197655386056417, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.c": 11535375410441493281, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/__init__.py": 5118993772178969104, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_setops.py": 13440916133402971190, - "backend/src/core/__tests__/test_native_filters.py": 823253235973274872, - "backend/src/core/superset_client/_dashboards_filters.py": 11002883972618719697, - "venv/lib/python3.13/site-packages/pip/_internal/network/__init__.py": 1586148254475575526, - "venv/lib/python3.13/site-packages/pygments/lexers/slash.py": 8320676560518108343, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/authorization_server.py": 4427151727224472650, - "venv/lib/python3.13/site-packages/pydantic/_internal/_forward_ref.py": 10623110912945011703, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_holiday.py": 8541197358003711845, - "backend/tests/test_translate_scheduler_execution.py": 16687335504320865378, - "frontend/build/_app/immutable/entry/app.EqiC6C3A.js": 12263353332086817548, - "venv/lib/python3.13/site-packages/ecdsa/errors.py": 11755424742691057760, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286_bc.pyf": 10893958728966010450, - "venv/lib/python3.13/site-packages/PIL/ImageColor.py": 17058618058554970738, - "venv/lib/python3.13/site-packages/urllib3/connectionpool.py": 7493427857878469165, - "venv/lib/python3.13/site-packages/click/termui.py": 7022374827798605437, - "venv/lib/python3.13/site-packages/pandas/core/reshape/tile.py": 17137524194129840833, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/__init__.py": 15340047930888693984, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_unique.py": 16285418015531891530, - "venv/lib/python3.13/site-packages/fastapi/middleware/gzip.py": 436886407566263171, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/char.pyi": 1639810799219814643, - "venv/lib/python3.13/site-packages/pandas/core/methods/selectn.py": 13552176539021625776, - "venv/lib/python3.13/site-packages/git/refs/reference.py": 2038789918710608481, - "frontend/src/components/DashboardGrid.svelte": 5332611872674866671, - "frontend/src/lib/components/translate/BulkCorrectionSidebar.svelte": 5119156213531888283, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_riscv_unix.h": 8607317131295864391, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.py": 17993169044119596860, - "venv/lib/python3.13/site-packages/starlette/middleware/base.py": 16850428285382256618, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/simple.py": 11050636051019777317, - "venv/lib/python3.13/site-packages/passlib/crypto/des.py": 14699826058580193499, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_get_numeric_data.py": 11747933921770969706, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/index_tricks.py": 10051261976498916957, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_linux.h": 4071314837793906998, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/palette.py": 17383863392192572184, - "venv/lib/python3.13/site-packages/apscheduler/executors/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/_postgres_builtins.py": 2383906454995727700, - "venv/lib/python3.13/site-packages/typing_inspection/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/ecdsa/test_der.py": 7117444285097824495, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jws_algs.py": 11920621906391796226, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayprint.py": 13335063050941743734, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py": 4559483361564472117, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_writers.py": 103545128256076790, - "venv/lib/python3.13/site-packages/multipart/__init__.py": 6496114190873890851, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/dml.py": 5340326603935367534, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/request.py": 17642741955458391050, - "frontend/src/components/git/ConflictResolver.svelte": 12431368407090802392, - "frontend/build/_app/immutable/nodes/23.Cla0r9L2.js": 4312072173612601380, - "frontend/build/_app/immutable/chunks/DepzS_bZ.js": 2243340878436625890, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/INSTALLER": 17282701611721059870, - "backend/src/core/utils/__init__.py": 12454773556575864508, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/WHEEL": 16097436423493754389, - "frontend/src/lib/components/layout/__tests__/test_taskDrawer.svelte.js": 15288009749873977442, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_constructors.pyi": 4993066609272697419, - "venv/lib/python3.13/site-packages/starlette/endpoints.py": 3379755567024530053, - "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.py": 7658234162211356265, - "venv/lib/python3.13/site-packages/starlette/schemas.py": 697416731579555195, - "venv/lib/python3.13/site-packages/pandas/core/internals/blocks.py": 6448406236246476333, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.py": 1472021608501071055, - "venv/lib/python3.13/site-packages/git/cmd.py": 8854786163152637148, - "venv/lib/python3.13/site-packages/passlib/handlers/digests.py": 18158237109145814857, - "venv/lib/python3.13/site-packages/pandas/tests/extension/json/array.py": 7437107031079833232, - "venv/lib/python3.13/site-packages/PIL/DdsImagePlugin.py": 10646832373516499548, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/conftest.py": 4124267277224879990, - "venv/lib/python3.13/site-packages/secretstorage/util.py": 1004887530903069622, - "venv/lib/python3.13/site-packages/passlib/handlers/scrypt.py": 10089372287597257402, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/compiler.py": 11662494978292401568, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/anyio/pytest_plugin.py": 4165280709813718099, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/constants.pyi": 15633065380510755171, - "backend/src/core/migration/risk_assessor.py": 1061977005072298440, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/pygments/lexers/elm.py": 14683437310498104916, - "backend/src/plugins/translate/__tests__/test_executor.py": 7200474786659302788, - "venv/lib/python3.13/site-packages/rpds/__init__.pyi": 12037474523494903587, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_misc.pyi": 7544619194032232861, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libssl-81ffa89e.so.3": 13913673119166658781, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/hooks.py": 9888227370911151341, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/RECORD": 18294169019244826301, - "venv/lib/python3.13/site-packages/pandas/_libs/window/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_utils/__init__.pyi": 13530522898409268883, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_arrow.py": 10068866425352207113, - "venv/lib/python3.13/site-packages/websockets/legacy/exceptions.py": 16025779645869134942, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/strings.pyi": 402863478592154500, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/elements.py": 11185011314498348980, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/__init__.py": 260774374497653876, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/METADATA": 8128818962678047497, - "frontend/src/components/StartupEnvironmentWizard.svelte": 6947772062866928838, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ec.pyi": 9835891299056543360, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/resource_owner_password_credentials.py": 9559295461619813890, - "venv/lib/python3.13/site-packages/numpy/lib/introspect.pyi": 12239924839764265507, - "venv/pyvenv.cfg": 2513607356761047446, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_tzconversion.py": 7205566353454584259, - "venv/lib/python3.13/site-packages/urllib3/util/url.py": 15992369651965016538, - "backend/src/services/__tests__/test_encryption_manager.py": 7097521778855634174, - "backend/test_app.db": 2486636812009447051, - "venv/lib/python3.13/site-packages/pillow.libs/libpng16-4a38ea05.so.16.53.0": 13136089038268964667, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot.py": 334735766122314163, - "venv/lib/python3.13/site-packages/anyio/abc/_subprocesses.py": 4297459042599291531, - "venv/lib/python3.13/site-packages/pydantic/__init__.py": 11536052293866174672, - "venv/lib/python3.13/site-packages/pandas/tests/tools/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/iniconfig/_version.py": 8497474654470220170, - "venv/lib/python3.13/site-packages/pandas/io/formats/style_render.py": 13586834368514801953, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_dt_accessor.py": 2229616362327032253, - "venv/lib/python3.13/site-packages/pygments/lexers/procfile.py": 3860651022061090738, - "venv/lib/python3.13/site-packages/yaml/serializer.py": 1783761365703703558, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_equivalence.py": 17843623343419137458, - "venv/lib/python3.13/site-packages/pygments/lexers/ncl.py": 14838368780033177943, - "venv/lib/python3.13/site-packages/numpy/tests/test_lazyloading.py": 17465787495701737655, - "venv/lib/python3.13/site-packages/websockets/sync/messages.py": 15699091325918681038, - "venv/lib/python3.13/site-packages/numpy/lib/user_array.py": 14003456655595459585, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/interface.py": 3134071530627935964, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_fsspec.py": 6936167344144998172, - "backend/tests/test_translate_scheduler.py": 14274816721941617887, - "backend/ss_tools_backend.egg-info/PKG-INFO": 15134841326777953914, - "venv/lib/python3.13/site-packages/cffi/error.py": 8147505969912539538, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.asm": 16630507336149184509, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_take.py": 9847808489301498487, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_put.py": 8547660958963625817, - "venv/lib/python3.13/site-packages/numpy/_typing/_nested_sequence.py": 7827259347937485408, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/cffi/_embedding.h": 9441285871823018603, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype.pyi": 17662618580381221543, - "venv/lib/python3.13/site-packages/cffi/__init__.py": 13427266446746498548, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/__init__.py": 5863110456909134678, - "venv/lib/python3.13/site-packages/pip/_internal/build_env.py": 3614358934066127900, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/api.py": 13134394673408254097, - "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.py": 17192211156810267818, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arraymethod.py": 14421188237635557797, - "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.cpython-313-x86_64-linux-gnu.so": 16604252871044637047, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f90continuation.f90": 17056591497459381871, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isin.py": 14387850595546682314, - "venv/lib/python3.13/site-packages/numpy/lib/_datasource.pyi": 5894423389796456573, - "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.pyi": 15509415708991012734, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reduce.py": 14976712507854054776, - "venv/lib/python3.13/site-packages/jeepney/tests/test_auth.py": 15125055415702968414, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_to_offset.py": 3929599794655343911, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parsing.py": 2228959015420278458, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_parser.py": 3540203683732744499, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/METADATA": 10220680536734745103, - "venv/lib/python3.13/site-packages/PIL/IcoImagePlugin.py": 14483964191652228364, - "venv/lib/python3.13/site-packages/PIL/SgiImagePlugin.py": 6916200065790425497, - "venv/lib/python3.13/site-packages/PIL/XVThumbImagePlugin.py": 7472134175570205454, - "venv/lib/python3.13/site-packages/pygments/lexers/diff.py": 5930189593946259160, - "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.py": 503331864022631297, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.cpython-313-x86_64-linux-gnu.so": 9014806524727977333, - "venv/lib/python3.13/site-packages/packaging/pylock.py": 15346942039643310253, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/windows.py": 16452996247375488034, - "venv/lib/python3.13/site-packages/passlib/handlers/bcrypt.py": 8125664010268654055, - "venv/bin/jsonschema": 16516374846693491442, - "frontend/postcss.config.js": 5714274776976071678, - "frontend/build/_app/immutable/nodes/37.h0kspZQz.js": 2116024229457453594, - "venv/lib/python3.13/site-packages/fastapi/security/__init__.py": 1345944646061334046, - "frontend/build/_app/immutable/chunks/B3G5yh93.js": 9701033449616441331, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_freq_attr.py": 1720704043778272895, - "venv/lib/python3.13/site-packages/click/_compat.py": 14451519072991457993, - "venv/lib/python3.13/site-packages/passlib/tests/__main__.py": 16477180140297957650, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_slice.py": 9936085512921529417, - "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.cpython-313-x86_64-linux-gnu.so": 16963890771704942465, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_concat.py": 16549010224568175202, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/licenses/LICENSE": 5563356551734678258, - "venv/lib/python3.13/site-packages/jeepney/bus_messages.py": 5871341372541525842, - "venv/lib/python3.13/site-packages/keyring/backends/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/schema.py": 5391655516870611151, - "frontend/build/_app/immutable/nodes/28.BMMMq5av.js": 1258679536261114563, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/regexopt.py": 16073437303824961930, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/uvicorn/logging.py": 12858188863836778374, - "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.py": 2215312866861772848, - "venv/lib/python3.13/site-packages/pydantic/main.py": 14086938579850319884, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_insert.py": 15536994417338770457, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_datetime.py": 13781712980921881470, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_update.py": 9098258267671647783, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/AUTHORS": 17022185907188244883, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/__init__.py": 8807410829260170597, - "frontend/build/_app/immutable/chunks/DsAODWVW.js": 17093454341120407063, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/attributes.py": 5068814626384132900, - "frontend/vitest.config.js": 142965001777137074, - "backend/src/plugins/migration.py": 16567640288127443175, - "backend/alembic/versions/b1c2d3e4f5a6_add_source_hash_to_translation_records.py": 4775534557742900399, - "venv/lib/python3.13/site-packages/pandas/io/excel/_base.py": 1251545894005373734, - "venv/lib/python3.13/site-packages/uvicorn/_compat.py": 2593000889619875759, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_strptime.py": 14328510310436552635, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_initialstub_already_started.py": 5562030563802006923, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.py": 1731362089136926666, - "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.py": 15558318749939680074, - "venv/lib/python3.13/site-packages/passlib/ext/django/__init__.py": 11713862539944289631, - "venv/lib/python3.13/site-packages/pandas/tests/test_common.py": 8987965560487646705, - "venv/lib/python3.13/site-packages/jose/utils.py": 1926138853970725083, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_unique.py": 3148372351781648868, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.py": 17817123894903120316, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape.pyi": 17197248408326188931, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_period_index.py": 6634414375581820058, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_iter.py": 9232021002336641851, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_freq_attr.py": 17290447066681837965, - "venv/lib/python3.13/site-packages/pandas/tests/frame/conftest.py": 15090201792072045503, - "venv/lib/python3.13/site-packages/pydantic/v1/error_wrappers.py": 5701679786078971939, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_dict_compat.py": 18196543090455290455, - "venv/lib/python3.13/site-packages/pandas/io/sas/sas7bdat.py": 17851617851457266799, - "venv/lib/python3.13/site-packages/starlette/concurrency.py": 9835859079437525910, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/idnadata.py": 1169745920682458213, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraysetops.py": 10588903945244480732, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/_internal_utils.py": 4091305333167213279, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_astype.py": 15768655715952923925, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py": 9673372914396625461, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_msvc.h": 14580207065003376324, - "venv/lib/python3.13/site-packages/pandas/tests/test_nanops.py": 8709105600366913011, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/named_types.py": 6099185243355915896, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/METADATA": 3448868793744358515, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2.py": 1331438198531942242, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.pyx": 5760108632332714287, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_macosx.h": 3653129798702716366, - "venv/lib/python3.13/site-packages/yaml/resolver.py": 6669674521776449098, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/RECORD": 11541505360973709787, - "venv/lib/python3.13/site-packages/jaraco/functools/__init__.pyi": 2872039192528394677, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo90.f90": 15981076495193915107, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/METADATA": 2053484635654179443, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_extract.py": 14547819021277949280, - "venv/lib/python3.13/site-packages/pygments/lexers/_luau_builtins.py": 13690910306072616818, - "venv/lib/python3.13/site-packages/pygments/lexers/parasail.py": 7758704230750989282, - "venv/lib/python3.13/site-packages/urllib3/util/retry.py": 17988032431204662756, - "venv/lib/python3.13/site-packages/pygments/lexers/rnc.py": 9747352274729854620, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.f": 18215492757907465667, - "backend/src/core/database.py.bak": 14909628735913201532, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/connection.py": 15405047955974108860, - "venv/lib/python3.13/site-packages/pygments/lexers/textedit.py": 14872291021083255414, - "backend/src/plugins/llm_analysis/__init__.py": 9254812131558249924, - "venv/lib/python3.13/site-packages/pygments/lexers/rebol.py": 10460732731157940232, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/pubprivmod.f90": 8680230823849248800, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/signals.py": 13677199634497673845, - "venv/lib/python3.13/site-packages/passlib/tests/test_totp.py": 9150389741890748343, - "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.pyi": 3661113433078225952, - "venv/lib/python3.13/site-packages/pygments/lexers/configs.py": 7712797939104760185, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/__init__.py": 2635182244050911910, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/base.py": 10262717073058743378, - "venv/lib/python3.13/site-packages/PIL/__init__.py": 10393984345353752724, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_concat.py": 4472503149811617440, - "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/__init__.py": 63497920264089252, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_convert_dtypes.py": 5005259275646154894, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlrd.py": 9982457234958102003, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/PIL/FontFile.py": 112053841436523305, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_union_categoricals.py": 6843679254735807, - "venv/lib/python3.13/site-packages/pygments/lexers/c_like.py": 13004061780700827047, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/constants.pyi": 4415188581374004936, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/file_proxy.py": 17695135735211897444, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.cpython-313-x86_64-linux-gnu.so": 2311012065007769341, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/__init__.py": 6327819603201859020, - "venv/lib/python3.13/site-packages/pygments/lexers/python.py": 8995519134978505744, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py": 7573043358244876807, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssltransport.py": 8148652555908329980, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_printing.py": 1901396869745313303, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/METADATA": 10241403930811743893, - "frontend/src/lib/components/reports/__tests__/report_type_profiles.test.js": 498330292141991614, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/resource_protector.py": 14354891358060085360, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/wrapper.py": 14599195010683170123, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/distro.py": 12494639376332486204, - "venv/lib/python3.13/site-packages/cryptography/hazmat/__init__.py": 6534110674636458866, - "venv/lib/python3.13/site-packages/dateutil/zoneinfo/rebuild.py": 1813298167468704155, - "venv/lib/python3.13/site-packages/packaging/utils.py": 3469957615920103670, - "frontend/build/_app/immutable/nodes/5.B73cStqs.js": 12655203259752489603, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timestamp.py": 10308799534491190078, - "frontend/src/components/DynamicForm.svelte": 12658559315809907844, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nditer.pyi": 2351219156711800159, - "frontend/public/vite.svg": 13568221685541582385, - "backend/src/services/clean_release/approval_service.py": 12912719831239691496, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_logical.py": 6242457515917145500, - "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.py": 2234931733687557433, - "backend/src/services/dataset_review/repositories/session_repository.py": 4058785536575367912, - "frontend/build/_app/immutable/nodes/24.BP-Fp4AB.js": 2248231906051278481, - "venv/lib/python3.13/site-packages/annotated_types/__init__.py": 7367790549779882749, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/__init__.py": 9482393721453335937, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/comparisons.py": 7327629888601690759, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/METADATA": 10707390829735099042, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py": 10505346210114745037, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/base.py": 13444129650880426353, - "frontend/src/lib/components/dataset-review/__tests__/us3_execution_batch.ux.test.js": 13416996904332574966, - "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/array.py": 13040461386712513615, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_struct_accessor.py": 6567451952835390075, - "venv/lib/python3.13/site-packages/attr/validators.pyi": 15579625896285277321, - "venv/lib/python3.13/site-packages/PIL/ImageMode.py": 2361901066832921091, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_selectable_constructors.py": 84919308502240880, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/__init__.py": 9820770319106921495, - "backend/conftest.py": 16521922883434363388, - "venv/lib/python3.13/site-packages/websockets/datastructures.py": 5180442040351387281, - "venv/lib/python3.13/site-packages/pydantic/v1/validators.py": 1060954771151908883, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_constructors.py": 1697610790604400105, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_duplicated.py": 14676058112867601208, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/WHEEL": 16097436423493754389, - "frontend/src/routes/settings/__tests__/settings_page.ux.test.js": 8756428349518151160, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_delitem.py": 16538190861492829338, - "venv/lib/python3.13/site-packages/cryptography/x509/certificate_transparency.py": 11337143186866439208, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-2.csv": 7063857529719738394, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nlargest.py": 14518132768753249360, - "venv/lib/python3.13/site-packages/websockets/sync/client.py": 10892557975919033983, - "venv/lib/python3.13/site-packages/pillow.libs/libopenjp2-94e588ba.so.2.5.4": 2483101806045986252, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccos.csv": 3026156288992132911, - "backend/test.db": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/_methods.pyi": 9155045948490225716, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_sparc_sun_gcc.h": 2915128971664712180, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/_mapping.py": 13335227913146542481, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/dotenv/main.py": 7408889670575271484, - "venv/lib/python3.13/site-packages/pandas/_libs/json.pyi": 10989301892466547548, - "venv/lib/python3.13/site-packages/PIL/_binary.py": 14039167720582222043, - "backend/src/core/migration/__init__.py": 13075189211840127611, - "venv/lib/python3.13/site-packages/attrs/converters.py": 13109003765349201809, - "venv/lib/python3.13/site-packages/pandas/util/_test_decorators.py": 17930801441803090445, - "venv/lib/python3.13/site-packages/fastapi/applications.py": 4975013401500183420, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_ops.py": 3875181889032080505, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_app.py": 4329794049222609894, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/state_changes.py": 11425629729736599320, - "venv/lib/python3.13/site-packages/PIL/ImageGrab.py": 11114379915762058868, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic/v1/utils.py": 902648247561165429, - "venv/lib/python3.13/site-packages/pygments/lexers/ml.py": 7510453371068494792, - "venv/lib/python3.13/site-packages/packaging/_elffile.py": 9546225990833172494, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/providers.py": 7267821734584612032, - "backend/src/api/routes/migration.py": 6030566471113208264, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/RECORD": 9461427699726982537, - "venv/lib/python3.13/site-packages/pip/_internal/utils/compatibility_tags.py": 15862649917149158291, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayterator.pyi": 7439348655567232744, - "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.pyi": 4729765666018003634, - "frontend/src/routes/reports/llm/[taskId]/report_page.contract.test.js": 4825093389030464765, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_chaining_and_caching.py": 15814259766479305290, - "venv/lib/python3.13/site-packages/anyio/to_process.py": 16627578499791368365, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp.cpython-313-x86_64-linux-gnu.so": 14187094676876471848, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarraytypes.h": 6765088137325865789, - "venv/lib/python3.13/site-packages/pandas/core/groupby/__init__.py": 3015263524218708013, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/foo_deps.f90": 14350239197565389508, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/cursor.py": 8074527784886981710, - "venv/lib/python3.13/site-packages/git/objects/blob.py": 8972374723153211283, - "backend/src/core/superset_client/_datasets_preview_filters.py": 12835417730902013107, - "venv/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py": 3531093454660385247, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libsasl2-883649fd.so.3.0.0": 13306917506408577991, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/__init__.py": 14571251344510763303, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/metadata.py": 1362166613223529108, - "venv/lib/python3.13/site-packages/numpy/lib/format.py": 14971167445246135440, - "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pxd": 9126772093305708921, - "venv/lib/python3.13/site-packages/requests/exceptions.py": 6474239743209354989, - "venv/lib/python3.13/site-packages/jeepney/tests/test_bus.py": 4260827599660763641, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/RECORD": 16342326945648353387, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.obj": 2884737706935637437, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayprint.py": 5912583751693302370, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py": 11393911418463808195, - "venv/lib/python3.13/site-packages/apscheduler/executors/twisted.py": 2695602672085615762, - "venv/lib/python3.13/site-packages/pydantic/v1/typing.py": 13467331592583800299, - "venv/lib/python3.13/site-packages/pandas/compat/numpy/function.py": 18405870240333383757, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi": 1065808244870331598, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/gcp.py": 12328409672947966867, - "venv/lib/python3.13/site-packages/numpy/lib/__init__.pyi": 1537495515469182226, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_setops.py": 5208912445718807372, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_where.py": 16584772339591698754, - "venv/lib/python3.13/site-packages/pandas/tests/strings/conftest.py": 12065119814157197510, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/result.py": 12262298207886620908, - "venv/lib/python3.13/site-packages/PIL/PcxImagePlugin.py": 1691405132377495336, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/random/_generator.cpython-313-x86_64-linux-gnu.so": 15625019745021657670, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_function.py": 13492077564015386528, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_categorical.py": 12897027063618569795, - "venv/lib/python3.13/site-packages/pandas/io/formats/style.py": 13235403689320479165, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/block.f": 10727418304254548946, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_api.py": 16743710460729009914, - "venv/lib/python3.13/site-packages/secretstorage/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/anyio/_core/_typedattr.py": 17584691045493791349, - "scripts/gen_semantics.py": 11397714264377363083, - "venv/lib/python3.13/site-packages/jaraco/classes/ancestry.py": 11742194518360056470, - "venv/lib/python3.13/site-packages/git/objects/submodule/base.py": 10140490301897269395, - "venv/lib/python3.13/site-packages/urllib3/_request_methods.py": 13671055114247516883, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/multiprocess.py": 10047012560534876149, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/licenses/LICENSE": 3868190977070717994, - "backend/src/plugins/translate/_utils.py": 4311334421388625700, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_polynomial.pyi": 9134949577567498894, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_diff.py": 7420674239386436885, - "venv/lib/python3.13/site-packages/pydantic/json.py": 4109352930301629308, - "venv/lib/python3.13/site-packages/numpy/_core/overrides.pyi": 13121243505132110607, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_get.py": 10255752806860493777, - "venv/lib/python3.13/site-packages/authlib/oidc/discovery/well_known.py": 8259335680633926377, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/utils.py": 3710235316419562274, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_records.py": 6252761812228230408, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply_relabeling.py": 3930092097678339743, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg.py": 8023549900603235601, - "backend/src/services/clean_release/preparation_service.py": 7557861288058237966, - "venv/lib/python3.13/site-packages/numpy/lib/_datasource.py": 18136853038949826147, - "venv/lib/python3.13/site-packages/numpy/_pytesttester.py": 17737911110576747508, - "venv/lib/python3.13/site-packages/pandas/core/indexes/category.py": 18192569199634406001, - "venv/lib/python3.13/site-packages/pandas/core/resample.py": 13814298871925341617, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_transpose.py": 17983160984979323393, - "venv/lib/python3.13/site-packages/attrs/filters.py": 6844006230924162452, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/missing.py": 14589973874359353799, - "frontend/src/lib/components/dataset-review/__tests__/source_intake_panel.ux.test.js": 13445861056598482363, - "backend/tests/scripts/test_clean_release_tui_v2.py": 16893299982170941753, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/__init__.py": 14481526616887213120, - "venv/lib/python3.13/site-packages/pydantic/validate_call_decorator.py": 11130572937843915026, - "frontend/src/routes/translate/[id]/+page.svelte": 2871210084962736100, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/control.py": 2034518223411945438, - "venv/lib/python3.13/site-packages/websockets/utils.py": 17624459810483092475, - "backend/src/api/routes/__tests__/test_git_api.py": 43259826036955162, - "venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py": 14416705841810221305, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_boxplot_method.py": 7635131059308651529, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_palettes.py": 13495896173068490516, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta.py": 7742506012012398830, - "venv/lib/python3.13/site-packages/httpcore/_utils.py": 3255581092836273826, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dsa.py": 8079219938116542284, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_methods.py": 14881910898871057761, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_is_homogeneous_dtype.py": 15378147393056707851, - "backend/src/models/dataset_review_pkg/_filter_models.py": 8426742165406106645, - "venv/lib/python3.13/site-packages/urllib3/util/wait.py": 14629837381061032546, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_indexing.py": 2274965639578580723, - "venv/lib/python3.13/site-packages/numpy/ma/mrecords.pyi": 8945514863366384484, - "venv/lib/python3.13/site-packages/numpy/_core/tests/_natype.py": 12013743717306900860, - "venv/lib/python3.13/site-packages/dateutil/rrule.py": 398469817784230512, - "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.py": 3646474165295296669, - "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/apps.py": 4219758952914498116, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/json.py": 4660603129768960095, - "venv/lib/python3.13/site-packages/yaml/__init__.py": 11044333164208875121, - "venv/lib/python3.13/site-packages/httpx/_transports/asgi.py": 12335675489870745729, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_dtypes.py": 6831042272921908068, - "venv/lib/python3.13/site-packages/httpcore/__init__.py": 5897259569277532401, - "venv/lib/python3.13/site-packages/jose/backends/ecdsa_backend.py": 2684176977865283060, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/authorization_server.py": 6858244712625056421, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/__init__.py": 2791238481813240414, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/_testing/compat.py": 10172779814048259850, - "venv/lib/python3.13/site-packages/pygments/styles/igor.py": 2291640843781128270, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py": 3515468590005709183, - "venv/lib/python3.13/site-packages/pygments/lexers/comal.py": 769727871242464458, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/mod.py": 2054876501774773382, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/core.py": 14576109242055156193, - "venv/lib/python3.13/site-packages/PIL/MicImagePlugin.py": 13540523615822507963, - "venv/lib/python3.13/site-packages/_pytest/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/window/expanding.py": 14141093310397856286, - "venv/lib/python3.13/site-packages/pandas/api/typing/aliases.py": 11194270368147942988, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/git/index/base.py": 641556304637379636, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/build_tracker.py": 9389444024234384408, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/_pytest/raises.py": 8188402179490363767, - "venv/lib/python3.13/site-packages/numpy/_typing/_char_codes.py": 14162745886150625150, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/RECORD": 17545477863793260029, - "venv/lib/python3.13/site-packages/attr/_next_gen.py": 16353246941851015420, - "venv/lib/python3.13/site-packages/websockets/__main__.py": 11948055520555854616, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/algorithms.py": 17106903014205241774, - "venv/lib/python3.13/site-packages/pydantic/_internal/_fields.py": 2692892815545218335, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_semicolon_split.py": 8153770165829451525, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.cpython-313-x86_64-linux-gnu.so": 5441755022198996487, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_join.py": 17508073052750406350, - "venv/lib/python3.13/site-packages/git/repo/base.py": 13975465345838683533, - "venv/lib/python3.13/site-packages/attr/__init__.pyi": 13203080513025719049, - "venv/lib/python3.13/site-packages/pygments/lexers/automation.py": 8021926838024836904, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.pyi": 4587511199710546030, - "venv/lib/python3.13/site-packages/websockets/sync/utils.py": 15135020989207191168, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.pyx": 17138729833735744777, - "venv/bin/pyrsa-decrypt": 10929163076272054227, - "venv/bin/Activate.ps1": 1196103427939049710, - "frontend/src/lib/stores/__tests__/test_activity.js": 15599963775430388097, - "frontend/src/routes/settings/git/+page.svelte": 9547035239167234477, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/__init__.py": 12849371269359608058, - "venv/lib/python3.13/site-packages/pandas/core/indexes/extension.py": 7193879749426425951, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_duplicates.py": 9830936017197354309, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get_value.py": 389530025760539462, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_filter.py": 2388681477981649268, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_arithmetic.py": 13320788210970132239, - "venv/lib/python3.13/site-packages/pytest_asyncio/plugin.py": 13414541723622354359, - "backend/src/plugins/debug.py": 2035734953696955393, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpq-9b38f5e3.so.5.17": 9680474575710585826, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_common.f": 5913807803369559749, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.pyi": 9282436354926617898, - "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.pyi": 18253950285509265097, - "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.py": 11397521081216402594, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_overlap.py": 4202716587079637350, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_argsort.py": 7320661835159648143, - "frontend/build/_app/immutable/nodes/9.86weTBJx.js": 9420619046628747900, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_quantile.py": 11273403743157110261, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_partial_slicing.py": 15567512240469921599, - "venv/lib/python3.13/site-packages/pandas/io/formats/_color_data.py": 15614976586613738953, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_html.py": 12040484325988635032, - "backend/src/core/cot_logger.py": 3113579786931231764, - "backend/src/api/routes/llm.py": 6474757426346697754, - "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.pyi": 7674087576653964854, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_setops.py": 14246225281428284850, - "venv/lib/python3.13/site-packages/PIL/_avif.pyi": 18222325750818585549, - "frontend/src/routes/settings/git/__tests__/git_settings_page.ux.test.js": 5334904955024018051, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/prompt.py": 9561640601516933380, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_describe.py": 362374422228593281, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/client.py": 17305914656270781911, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexer.py": 14400766876373764268, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py": 7107619871245389835, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_astype.py": 4854809596041987380, - "backend/src/core/superset_client/_charts.py": 8101226087578568224, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/hstore.py": 9700253004947691961, - "venv/lib/python3.13/site-packages/ecdsa/test_rw_lock.py": 1572802971060634697, - "frontend/build/_app/immutable/chunks/GHK41q1Y.js": 2219327255070861874, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename_axis.py": 12432900682994622455, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test__datasource.py": 1747755531265478106, - "venv/lib/python3.13/site-packages/pygments/lexers/vyper.py": 6477559246065994199, - "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.cpython-313-x86_64-linux-gnu.so": 9380059465932560412, - "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.py": 12886275429547303345, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/__init__.py": 8558541715982163200, - "backend/src/api/routes/assistant/_llm_planner_intent.py": 10603216032586398176, - "venv/lib/python3.13/site-packages/pandas/tests/test_algos.py": 11860014896449369497, - "backend/src/api/routes/clean_release.py": 1984604735131504761, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/util.py": 4815334059917765852, - "venv/lib/python3.13/site-packages/websockets/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_partial.py": 15129217814262096441, - "venv/lib/python3.13/site-packages/ecdsa/_version.py": 17707102226558781833, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_spec_conformance.py": 16448324304809345527, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_hour.py": 14128508606361694814, - "venv/lib/python3.13/site-packages/PIL/QoiImagePlugin.py": 17663704273100331654, - "venv/lib/python3.13/site-packages/pygments/lexers/text.py": 10572727755736597715, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_range.py": 9900908262700749861, - "backend/src/services/clean_release/mappers.py": 8684357784135236729, - "venv/lib/python3.13/site-packages/_pytest/nodes.py": 515939386237558331, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_datetimes.py": 7889573945417063183, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_to_xml.py": 8477770807916990981, - "venv/lib/python3.13/site-packages/pydantic/_internal/_typing_extra.py": 14843908493613991959, - "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.pyi": 10638891136586198429, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/arrays/__init__.py": 10460719978124302175, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_period.py": 12593777226419608212, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_util.py": 6573034224727513309, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/state.py": 10500170103357297551, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_quarter.py": 11099677280196239755, - "venv/lib/python3.13/site-packages/numpy/exceptions.py": 10981418339663118022, - "venv/lib/python3.13/site-packages/uvicorn/lifespan/off.py": 16254524872582859561, - "backend/src/services/clean_release/facade.py": 11231787492455424065, - "venv/lib/python3.13/site-packages/PIL/ImageFont.py": 15621336334706847865, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/indexable.py": 4131796922597075758, - "venv/lib/python3.13/site-packages/requests/adapters.py": 17161109043243436200, - "venv/lib/python3.13/site-packages/apscheduler/triggers/date.py": 9545774812177853725, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/meson.build": 17468163147765825687, - "venv/lib/python3.13/site-packages/greenlet/tests/test_generator.py": 14156412320760444199, - "venv/lib/python3.13/site-packages/numpy/_pytesttester.pyi": 5750506937526308405, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex_like.py": 1407890639206728014, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/cells.py": 8299309127027168194, - "venv/lib/python3.13/site-packages/git/index/__init__.py": 7874728461616959441, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/_pytest/setuponly.py": 10959102379908419762, - "backend/src/plugins/translate/__tests__/test_preview.py": 12246698485015379020, - "venv/lib/python3.13/site-packages/pygments/lexers/clean.py": 17772169928835025327, - "venv/lib/python3.13/site-packages/pip/_internal/commands/__init__.py": 3795218871212151351, - "venv/lib/python3.13/site-packages/requests/models.py": 9104898985870652256, - "venv/lib/python3.13/site-packages/git/refs/symbolic.py": 14996822770181935589, - "venv/lib/python3.13/site-packages/PIL/ImageDraw2.py": 13610162107593278891, - "venv/lib/python3.13/site-packages/pyasn1/type/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/jsonschema/tests/fuzz_validate.py": 2328585939822059352, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_utils.py": 4612923732868929770, - "venv/lib/python3.13/site-packages/pandas/tests/base/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/anyio/_core/_subprocesses.py": 7271614043537119143, - "venv/lib/python3.13/site-packages/anyio/_backends/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/urllib3/util/ssltransport.py": 1175688460659745788, - "venv/lib/python3.13/site-packages/urllib3/util/util.py": 12490259045660543460, - "venv/lib/python3.13/site-packages/pygments/styles/material.py": 14325514369696961874, - "venv/lib/python3.13/site-packages/pydantic/_internal/_docs_extraction.py": 5210114991139191634, - "venv/lib/python3.13/site-packages/pygments/lexers/_scheme_builtins.py": 5722444920479775453, - "frontend/src/routes/admin/users/+page.svelte": 18167787254767604761, - "frontend/build/_app/immutable/chunks/CKGT5ge7.js": 12663886993086252818, - "venv/lib/python3.13/site-packages/numpy/random/_generator.pyi": 17167649524261942483, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_print.py": 598821365691498702, - "venv/lib/python3.13/site-packages/uvicorn/loops/uvloop.py": 5385425357504191161, - "venv/lib/python3.13/site-packages/rapidfuzz/utils_py.py": 12341806772632276946, - "venv/lib/python3.13/site-packages/pandas/io/formats/csvs.py": 17223598248259691292, - "venv/bin/pip3.13": 13861749540792881808, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/response.py": 353446217435156470, - "venv/lib/python3.13/site-packages/pandas/compat/_optional.py": 12714208019736797073, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_regression.py": 12593643817685777170, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/WHEEL": 14929202952940710322, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_interp_fillna.py": 5530041283991655415, - "docker/frontend.Dockerfile": 9514032629617942525, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/METADATA": 1266134223155266488, - "venv/bin/dotenv": 953953349803507198, - "backend/src/core/superset_client/_datasets.py": 4813403545392307482, - "venv/lib/python3.13/site-packages/bcrypt/__init__.py": 11415155211917181362, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_packbits.py": 7592956764084658562, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_indexing.py": 16291207621268530224, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/LICENSE": 16510279186416777136, - "backend/src/plugins/git_plugin.py": 12799126783361308681, - "venv/lib/python3.13/site-packages/jeepney/__init__.py": 1063800617763464773, - "venv/lib/python3.13/site-packages/gitdb/db/ref.py": 1176701624460337371, - "venv/lib/python3.13/site-packages/pydantic/decorator.py": 6984214881365537811, - "venv/lib/python3.13/site-packages/_pytest/_argcomplete.py": 3586514420674089892, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_version.py": 10384171076976071398, - "venv/lib/python3.13/site-packages/secretstorage/exceptions.py": 16701299261122689924, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2": 6199424505696679197, - "venv/lib/python3.13/site-packages/pygments/lexers/testing.py": 17148598391210353576, - "venv/lib/python3.13/site-packages/passlib/ext/django/models.py": 12386223748379294996, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/unused_registry.py": 10153622516112471812, - "venv/lib/python3.13/site-packages/httpx/_multipart.py": 2181282475656908730, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/diagnose.py": 17275977405061138181, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_export_format.py": 17862222951776199694, - "venv/lib/python3.13/site-packages/greenlet/tests/leakcheck.py": 10179165748538820557, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_iteration.py": 2118501258411211851, - "venv/lib/python3.13/site-packages/packaging/_structures.py": 13086687542872305890, - "venv/lib/python3.13/site-packages/jsonschema/_utils.py": 14237077105384631097, - "backend/src/api/routes/assistant/_dataset_review_dispatch.py": 10108558091463171824, - "backend/src/api/routes/assistant/_resolvers.py": 1139669723103531814, - "venv/lib/python3.13/site-packages/pandas/core/ops/common.py": 17314999825118928100, - "venv/lib/python3.13/site-packages/dateutil/tz/_factories.py": 7538654189286887518, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/AUTHORS": 3558407083075395848, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py": 16951268065271931235, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_index.py": 12015738970771703312, - "venv/lib/python3.13/site-packages/jsonschema/__main__.py": 18136546137047911521, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/__init__.py": 17454177076687312424, - "venv/lib/python3.13/site-packages/pip/_internal/cli/main.py": 9304034369762028739, - "backend/tests/core/test_mapping_service.py": 6888870177029085220, - "venv/lib/python3.13/site-packages/pandas/io/excel/_util.py": 6664989864423807444, - "frontend/src/lib/components/reports/__tests__/report_detail.integration.test.js": 4193615240297330385, - "venv/lib/python3.13/site-packages/referencing/exceptions.py": 2535322334430110414, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/mypy.ini": 4763385838822685881, - "frontend/build/_app/immutable/nodes/26.BpHj-_gK.js": 12667509163112762275, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/authlib/common/errors.py": 16082974228565515177, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/ecdsa/eddsa.py": 15966858338456571506, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/METADATA": 3044554568188160018, - "backend/src/services/__tests__/test_health_service.py": 5498183976797488518, - "venv/lib/python3.13/site-packages/jeepney/io/blocking.py": 4699555965372200059, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/METADATA": 14158024963562115116, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/dml.py": 6567627855667533645, - "frontend/build/_app/immutable/assets/AssistantChatPanel.D4L5jlt0.css": 10724144138366384732, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/pytestplugin.py": 1591034895268415944, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dot.py": 16876560325283070437, - "frontend/static/favicon.svg": 6451919037497541980, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_datetime.py": 7862725028865965163, - "venv/lib/python3.13/site-packages/pycparser/c_parser.py": 3954138286651284327, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_3kcompat.h": 5377305123299457337, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/transforms.py": 3773159138960141434, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937_regressions.py": 8981142329261965538, - "venv/lib/python3.13/site-packages/anyio/functools.py": 4300781896135050327, - "venv/lib/python3.13/site-packages/greenlet/greenlet_msvc_compat.hpp": 12504008513945919250, - "venv/lib/python3.13/site-packages/jeepney/tests/test_fds.py": 6257376331171761928, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_array_utils.py": 14682148205379122638, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_matmul.py": 16169282225324014901, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py": 6678500392137188521, - "venv/lib/python3.13/site-packages/pandas/_typing.py": 17795648994662434211, - "venv/lib/python3.13/site-packages/cffi/_cffi_include.h": 13094899814611950155, - "venv/lib/python3.13/site-packages/iniconfig/__init__.py": 12623265899068631092, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets.py": 10923534235697247269, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/endpoint.py": 18260428408051728802, - "venv/lib/python3.13/site-packages/pandas/tests/test_optional_dependency.py": 7930709646653025355, - "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.py": 16854377502491291093, - "venv/lib/python3.13/site-packages/itsdangerous/__init__.py": 6969210114978365656, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_numpy_compat.py": 9112925138440787555, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_asfreq.py": 12721047184483392773, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_parse_dates.py": 7597935994945336408, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py": 15928258358116399067, - "venv/lib/python3.13/site-packages/pandas/io/sas/__init__.py": 8095743504390517992, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/plugin.py": 16042078006552521613, - "venv/lib/python3.13/site-packages/PIL/_version.py": 3025206805667405876, - "backend/tests/test_dashboards_api.py": 2278794209397518464, - "frontend/build/_app/immutable/chunks/DSTrsXYC.js": 7992933864369085731, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_block_internals.py": 17935662848672401507, - "venv/lib/python3.13/site-packages/pydantic/_internal/_repr.py": 1758478941817868344, - "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/algorithms.py": 2375452080667815941, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_file_handling.py": 732772718210067599, - "frontend/build/_app/immutable/nodes/35.ClWaxdrE.js": 18139182329852451741, - "venv/lib/python3.13/site-packages/_pytest/legacypath.py": 7512065138259788426, - "venv/lib/python3.13/site-packages/pydantic/deprecated/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_transpose.py": 1686867045869336370, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/ext.py": 15699809032725838985, - "venv/lib/python3.13/site-packages/cryptography/utils.py": 18088508841978595788, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_integer.f90": 8308927438994838783, - "frontend/build/_app/immutable/nodes/4.CzF7b7T3.js": 5413823557660901896, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.pyi": 12925006572914814268, "venv/lib/python3.13/site-packages/passlib/handlers/roundup.py": 3844280972451753019, - "venv/lib/python3.13/site-packages/pygments/lexers/graphviz.py": 15677908982985025500, - "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.pyi": 10487441680587530350, - "venv/lib/python3.13/site-packages/_pytest/junitxml.py": 5871715626272366062, - "venv/lib/python3.13/site-packages/cffi/lock.py": 13507686939203117974, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_astype.py": 4975959187621745360, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_dtypes.py": 13065857166166716628, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filter.py": 16846063190466267618, - "venv/lib/python3.13/site-packages/pandas/io/json/__init__.py": 12333505155070172626, - "venv/lib/python3.13/site-packages/packaging/licenses/_spdx.py": 16440031845441362300, - "backend/src/models/auth.py.bak": 7050478675360007060, - "venv/lib/python3.13/site-packages/_pytest/pytester_assertions.py": 1899505062406979663, - "venv/lib/python3.13/site-packages/pydantic/v1/generics.py": 1150594223414883339, - "venv/lib/python3.13/site-packages/starlette/_utils.py": 8384087194580626069, - "venv/lib/python3.13/site-packages/httpcore/_sync/interfaces.py": 1532245423878428608, - "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.pyi": 9911899303567570646, - "backend/src/core/auth/config.py": 8065625913451826937, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/nditer.py": 10038360623032425884, - "venv/lib/python3.13/site-packages/pygments/lexers/actionscript.py": 8829560731160428621, - "venv/lib/python3.13/site-packages/referencing/typing.py": 8296636464219681747, - "venv/lib/python3.13/site-packages/rapidfuzz/_common_py.py": 15410157970973169427, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/char.pyi": 13143560686049627445, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_date_range.py": 16128552513324017349, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py": 16463243654123731993, - "venv/lib/python3.13/site-packages/numpy/lib/_version.pyi": 10804755840730584157, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_missing.py": 1963705411068820866, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/exc.py": 1448374203226983757, - "backend/src/models/__tests__/test_report_models.py": 14916807060334836854, - "venv/lib/python3.13/site-packages/pip/_internal/models/installation_report.py": 14534544543677907640, - "venv/lib/python3.13/site-packages/keyring/testing/util.py": 11195868112805409554, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/sphinxext.py": 2403717774564798878, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/licenses/LICENSE.rst": 5960743202700406649, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarrayobject.h": 7320510589635636690, - "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/npymath.ini": 1071371630695963467, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.cpython-313-x86_64-linux-gnu.so": 12473903676666680134, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_dialect.py": 4700488504252803159, - "venv/lib/python3.13/site-packages/greenlet/TStackState.cpp": 16720079760074325527, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_indexing.py": 546024765756472196, - "venv/lib/python3.13/site-packages/jose/backends/__init__.py": 16581763658239070112, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.py": 12636954896699095212, - "venv/lib/python3.13/site-packages/git/refs/__init__.py": 16246686106073400519, - "venv/lib/python3.13/site-packages/pydantic/deprecated/tools.py": 16466182352694741629, - "venv/lib/python3.13/site-packages/greenlet/slp_platformselect.h": 10428201768098464386, - "venv/lib/python3.13/site-packages/numpy/ma/testutils.pyi": 3899348061346041413, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_constructors.py": 9478848417896027917, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/INSTALLER": 17282701611721059870, - "backend/src/plugins/llm_analysis/__tests__/test_service.py": 9850768293526486431, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/formatters/terminal256.py": 11473927781167132440, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs7.pyi": 2777456956260371820, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nep50_promotions.py": 6318578947214302182, - "venv/lib/python3.13/site-packages/pandas/core/ops/dispatch.py": 7041711677358147534, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_array_ops.py": 6709502157317299163, - "venv/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py": 991029278889407558, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/baked.py": 6911138208603240867, - "frontend/src/lib/components/reports/ReportsList.svelte": 15460090587871611258, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/aead.pyi": 13061638961405460334, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py": 3177663126928467950, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/RECORD": 17594742868855466393, - "venv/lib/python3.13/site-packages/psycopg2/tz.py": 9966680685796585198, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/index_tricks.pyi": 6035507586990711471, - "venv/lib/python3.13/site-packages/pandas/tests/api/test_api.py": 13385690673368400635, - "venv/lib/python3.13/site-packages/uvicorn/middleware/message_logger.py": 14825516604555033580, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/bitwise_ops.pyi": 751490187478960891, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/METADATA": 3474626408138844944, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_info.py": 10615285239043745712, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_partial_slicing.py": 16219611534574666182, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_reduction.py": 1049551473665255648, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/__init__.py": 4337679908168515343, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/WHEEL": 16097436423493754389, - "backend/src/api/routes/__init__.py": 9441391336360835840, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/RECORD": 6187512110158566509, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_delitem.py": 14336697139511118920, - "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py": 6332087444349093252, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_numpyconfig.h": 18150055639397773687, - "venv/lib/python3.13/site-packages/pygments/lexers/hdl.py": 6772823608124143123, - "venv/lib/python3.13/site-packages/pygments/lexers/bqn.py": 11823648528816858738, - "venv/lib/python3.13/site-packages/git/objects/submodule/util.py": 8621897576747443758, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.pyi": 3783371827926657291, - "venv/lib/python3.13/site-packages/numpy/random/mtrand.cpython-313-x86_64-linux-gnu.so": 16146459667121787202, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/isocintrin/isoCtests.f90": 6208802899442270399, - "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/__init__.py": 1229691061011958769, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py": 10196543528439925412, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/__init__.py": 293004106962245584, - "venv/lib/python3.13/site-packages/pyasn1/type/useful.py": 10313387611927801339, - "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.py": 16539612003028676816, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/licenses/COPYING": 15270149301471737292, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py": 12245524932757110911, - "venv/lib/python3.13/site-packages/cryptography/x509/ocsp.py": 17973800752927680404, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log10.csv": 13441294827017400897, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_generic.py": 1297407268239966429, - "venv/lib/python3.13/site-packages/pycparser/ply/yacc.py": 4218889705926129841, - "venv/lib/python3.13/site-packages/jeepney/tests/test_bus_messages.py": 4323093138307306385, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_dists.py": 16746363359320577809, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_defchararray.py": 15135904718007443306, - "venv/lib/python3.13/site-packages/pygments/lexers/dotnet.py": 17970884907837213099, - "frontend/src/lib/components/translate/CorrectionCell.svelte": 11242772726843637793, - "venv/lib/python3.13/site-packages/_pytest/outcomes.py": 12782895027812738142, - "venv/lib/python3.13/site-packages/pandas/io/parsers/__init__.py": 11752833703934347122, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/coercions.py": 5112358449376598662, - "backend/src/core/task_manager/__init__.py": 15991802770215124472, - "venv/lib/python3.13/site-packages/passlib/handlers/sha2_crypt.py": 8171587560325812664, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_resolution.py": 9651416588266430520, - "venv/lib/python3.13/site-packages/numpy/core/defchararray.py": 11371045148957547088, - "venv/lib/python3.13/site-packages/pip/_internal/cli/base_command.py": 10660882478126415399, - "backend/src/services/clean_release/__tests__/test_preparation_service.py": 4958420988131940633, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_sorted.py": 1899608688401238388, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_update_delete.py": 1317337950621091021, - "venv/lib/python3.13/site-packages/pandas/_libs/join.pyi": 369262334342439364, - "venv/lib/python3.13/site-packages/pygments/formatters/bbcode.py": 8614391937525677922, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/schema.py": 8358664670620586363, - "frontend/src/components/llm/__tests__/provider_config.integration.test.js": 12605115626277870995, - "venv/lib/python3.13/site-packages/pandas/tests/computation/test_compat.py": 11655000938111420543, - "frontend/build/_app/immutable/nodes/32.BQVcp1Fc.js": 17523113223503452911, - "venv/lib/python3.13/site-packages/greenlet/_greenlet.cpython-313-x86_64-linux-gnu.so": 6036818030377883416, - "venv/lib/python3.13/site-packages/authlib/integrations/django_client/integration.py": 13610682193839598457, - "venv/lib/python3.13/site-packages/_pytest/faulthandler.py": 13329320306353718649, - "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.pyi": 15034917167424979097, - "backend/src/services/clean_release/repositories/artifact_repository.py": 9040122866024534865, - "venv/lib/python3.13/site-packages/pygments/cmdline.py": 15628499088643246809, - "frontend/src/components/Toast.svelte": 340258331315696736, - "venv/lib/python3.13/site-packages/pygments/lexers/matlab.py": 1880915927722177654, - "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.py": 338595621218646492, - "venv/lib/python3.13/site-packages/numpy/polynomial/_polytypes.pyi": 7528777458251688517, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_setops.py": 8622608042929141013, - "venv/lib/python3.13/site-packages/requests/utils.py": 17045260098091371478, - "frontend/src/routes/admin/settings/llm/+page.svelte": 8015678318492837753, - "frontend/build/_app/immutable/nodes/29.BgDRJ3OZ.js": 14521731666892374966, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/json.py": 15373167408891671190, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/filepost.py": 16698380375728063923, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh18335.f90": 1126438609189294255, - "frontend/src/components/Footer.svelte": 17748559656923032, - "backend/src/services/clean_release/repositories/policy_repository.py": 10990781278715170404, - "venv/lib/python3.13/site-packages/jsonschema/protocols.py": 847332300543820834, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_csv.py": 571991535656576735, - "venv/lib/python3.13/site-packages/_pytest/_code/source.py": 9488112979192923739, - "venv/lib/python3.13/site-packages/websockets/auth.py": 499195617702591757, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_masked_matrix.py": 1444179242134203344, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_timedeltas.py": 4118671843063545221, - "backend/src/services/clean_release/dto.py": 11031410734734770552, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/scalars.pyi": 17683675356659300769, - "venv/lib/python3.13/site-packages/more_itertools/__init__.py": 9858612105173082060, - "venv/lib/python3.13/site-packages/websockets/asyncio/async_timeout.py": 7267209982032642028, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/__init__.py": 13841777369501476946, - "venv/lib/python3.13/site-packages/passlib/context.py": 10362592407593745107, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_append.py": 17016125916761365423, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix_py.py": 17041764258638228993, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_openssl.py": 10446441599800965961, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_mrecords.py": 15952147778503828707, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_longdouble.py": 9337218715436213122, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_reduction.py": 2671872764983553906, - "backend/src/api/routes/assistant/_dispatch.py": 3811422882986895565, - "venv/lib/python3.13/site-packages/_pytest/_io/__init__.py": 1510478951261938348, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_freq_code.py": 13758353172962109140, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/wait.py": 6196757964348666082, - "venv/lib/python3.13/site-packages/pycparser/ply/__init__.py": 5040371336745852622, - "backend/src/services/profile_service.py": 10016788771873430731, - "venv/lib/python3.13/site-packages/numpy/f2py/__init__.pyi": 3566887042414273892, - "venv/lib/python3.13/site-packages/pygments/lexers/textfmts.py": 17438453283146671997, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/constant_time.py": 2499026008403328245, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_both.f90": 13943066317159236257, - "venv/lib/python3.13/site-packages/fastapi/_compat/v2.py": 15078491685917047095, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_floatingpoint_errors.py": 4301355720042319835, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/requests.py": 1102963893193914979, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.pyi": 13173581775000480811, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/cast.py": 342228968652072052, - "venv/lib/python3.13/site-packages/numpy/core/getlimits.py": 14257687540182552408, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE": 16499431172938528491, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_cpp.cpython-313-x86_64-linux-gnu.so": 7490281502544276964, - "venv/lib/python3.13/site-packages/numpy/_core/multiarray.py": 4484782354376574355, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_apply.py": 11248068903302454630, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_twodim_base.py": 17623133613018058380, - "venv/lib/python3.13/site-packages/psycopg2/pool.py": 8855184580138330938, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/py.typed": 9796674040111366709, - "venv/lib/python3.13/site-packages/numpy/f2py/rules.py": 9322914583460630877, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/packaging/__init__.py": 5479277484104486860, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_ssl_constants.py": 14834514243239777933, - "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending.py": 16515485226251040454, - "venv/lib/python3.13/site-packages/numpy/ma/LICENSE": 6723325434476453127, - "venv/lib/python3.13/site-packages/websockets/extensions/permessage_deflate.py": 12957105912018691390, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_groupby_shift_diff.py": 1210657479716368827, - "venv/lib/python3.13/site-packages/passlib/tests/test_apps.py": 10454136535890937237, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/type_api.py": 10299750173867399533, - "venv/lib/python3.13/site-packages/pycparser/lextab.py": 16045770806700381661, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.pyi": 4432290176776357346, - "venv/lib/python3.13/site-packages/cffi/parse_c_type.h": 9889309594462820068, - "venv/lib/python3.13/site-packages/pandas/_libs/algos.cpython-313-x86_64-linux-gnu.so": 15706331654215762588, - "venv/lib/python3.13/site-packages/yaml/events.py": 745772917931702910, - "venv/lib/python3.13/site-packages/numpy/tests/test_reloading.py": 17532116685329953400, - "venv/lib/python3.13/site-packages/charset_normalizer/__init__.py": 12412600648990392633, - "backend/src/core/utils/superset_context_extractor/_parsing.py": 17093712504833649098, - "venv/lib/python3.13/site-packages/pandas/core/groupby/generic.py": 4857114071347950833, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_timedeltaindex.py": 2359681317403710563, - "venv/lib/python3.13/site-packages/pandas/core/reshape/api.py": 16364136812252343076, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/core": 10616484054735611337, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/exceptions.py": 7518613450390473990, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24008.f": 3872245077994936272, - "venv/lib/python3.13/site-packages/fastapi/concurrency.py": 17937688838625807004, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/sum_.py": 7062070517544958546, - "venv/lib/python3.13/site-packages/rsa/common.py": 10921682819204792805, - "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.abi3.so": 8691731321358529264, - "venv/lib/python3.13/site-packages/numpy/_configtool.pyi": 9670892208858157218, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/take.py": 3170415198452493008, - "venv/lib/python3.13/site-packages/pandas/tests/computation/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_base_indexer.py": 3129123098288498633, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_astype.py": 15275720655739710249, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval_pyarrow.py": 3849382723890414899, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np121.pkl.gz": 4708588000144933291, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/types.py": 16795741817129898714, - "venv/lib/python3.13/site-packages/PIL/FtexImagePlugin.py": 8196606399875185931, - "venv/lib/python3.13/site-packages/yaml/reader.py": 9873075843010693205, - "venv/lib/python3.13/site-packages/PIL/GribStubImagePlugin.py": 14954946414134984896, - "venv/lib/python3.13/site-packages/httpcore/_models.py": 2501866419898326733, - "frontend/build/_app/immutable/chunks/BIIQCMQD.js": 10069504695005052902, - "backend/src/api/routes/translate/_router.py": 4164547666727936398, - "backend/src/plugins/storage/__init__.py": 5046554941488079204, - "venv/lib/python3.13/site-packages/_pytest/config/argparsing.py": 11156052029258891503, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.py": 15511055314565873928, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_serialization.py": 2011519400081029270, - "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.pyi": 12566839271245057531, - "venv/lib/python3.13/site-packages/passlib/handlers/argon2.py": 835074952952017434, - "venv/lib/python3.13/site-packages/starlette/formparsers.py": 9019779181100355515, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_list_accessor.py": 8613662386779664039, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/__init__.py": 17194870661869873083, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_common.h": 14732581790650403546, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_indexing.py": 14461606056665485421, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/certs.py": 13366788049827102313, - "venv/lib/python3.13/site-packages/numpy/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_pydatetime.py": 16228315407662560992, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_skiprows.py": 8764515842253490735, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_explode.py": 16195468192763502449, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py": 1131520422778420758, - "venv/lib/python3.13/site-packages/pandas/io/excel/_calamine.py": 13354427767660487637, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_routines.py": 4860173574365060471, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/screen.py": 6933591906297023845, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategy_options.py": 16342048970907383117, - "venv/lib/python3.13/site-packages/fastapi/dependencies/utils.py": 7687214523082385505, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-1.csv": 16349400027062875699, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py": 17786759398959482432, - "venv/lib/python3.13/site-packages/pydantic/validators.py": 8292819706488319845, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/authorization_code.py": 4958605540994687737, - "venv/lib/python3.13/site-packages/_pytest/pathlib.py": 9719734558816066453, - "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.py": 15031197044862908440, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_mod.f90": 5765181406654284736, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/operators.py": 5584982192355398255, - "venv/lib/python3.13/site-packages/pygments/lexers/data.py": 9365024327689738663, - "frontend/src/routes/dashboards/+page.svelte": 7340914202224737248, - "backend/src/services/clean_release/manifest_builder.py": 17216832960644499305, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_liboffsets.py": 14705822637347212540, - "venv/lib/python3.13/site-packages/pip/_internal/commands/show.py": 11839987586497822300, - "venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py": 3095012838116670328, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_stringdtype.py": 8381476847196129074, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dtype.py": 5336980126536664360, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/RECORD": 11604408378998955169, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_constructors.py": 6515602441449955627, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/errors.py": 12935154705443176852, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_missing.py": 6224750552277937853, - "venv/lib/python3.13/site-packages/itsdangerous/encoding.py": 205863946173486958, - "venv/lib/python3.13/site-packages/pydantic/deprecated/parse.py": 819235411736647763, - "venv/lib/python3.13/site-packages/greenlet/TGreenletGlobals.cpp": 17400526028982558937, - "frontend/src/routes/settings/connections/+page.svelte": 276661107137418305, - "venv/lib/python3.13/site-packages/greenlet/tests/test_contextvars.py": 6077482318964702132, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/models.py": 17356033861792227817, - "venv/lib/python3.13/site-packages/referencing/retrieval.py": 11921179805977833120, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/resource_protector.py": 3563910821279222695, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/theme.py": 14225878259183479398, - "venv/lib/python3.13/site-packages/idna/core.py": 7725813731640753267, - "venv/lib/python3.13/site-packages/passlib/handlers/django.py": 66638338316838351, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_cov_corr.py": 8117511855230249212, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_manylinux.py": 12272275001961360101, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/engines.py": 10876954465293959720, - "frontend/build/_app/immutable/nodes/0.Dy6ixPTE.js": 12881323989915888235, - "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.pyi": 7171810177801103765, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/aioodbc.py": 12704729841432715246, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/ansi.py": 948249091929588017, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_getlimits.py": 118041882543413669, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py": 2492962412506082234, - "frontend/build/_app/immutable/chunks/DSaq_U9T.js": 18007300971386663499, - "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_reductions.py": 11624188672428941494, - "backend/src/api/routes/dashboards/_listing_routes.py": 1750634467656242702, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/plugin_base.py": 2880434680251108786, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/__init__.py": 16532856661318424631, - "venv/lib/python3.13/site-packages/python_multipart/exceptions.py": 5971058243218049183, - "venv/lib/python3.13/site-packages/pandas/io/parsers/base_parser.py": 15148258424429418380, - "venv/lib/python3.13/site-packages/idna/uts46data.py": 5698404064763638591, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/evaluator.py": 6348022789899831255, - "venv/lib/python3.13/site-packages/starlette/status.py": 16192064443996706569, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/interfaces.py": 4890958419958257958, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_aix.h": 8706347224105630468, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/comparisons.pyi": 4158883009780474220, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_assignability.pyi": 1718610984396277997, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/_typing.py": 17598638528781632158, - "venv/lib/python3.13/site-packages/git/remote.py": 1694919255653049944, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_deprecations.py": 16367195310156359251, - "venv/lib/python3.13/site-packages/smmap/mman.py": 9091562788592406032, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/events.py": 7576713358952064464, - "venv/lib/python3.13/site-packages/jaraco/context/__init__.py": 10975097417056278427, - "venv/lib/python3.13/site-packages/pandas/util/version/__init__.py": 3844072588696074141, - "venv/lib/python3.13/site-packages/pycparser/ast_transforms.py": 9032453625977464506, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_exceptions.py": 15290185665769490169, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/RECORD": 173225030102092319, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/__init__.py": 913610541217778747, - "venv/lib/python3.13/site-packages/numpy/_core/__init__.pyi": 2783897020308040960, - "venv/lib/python3.13/site-packages/keyring/compat/properties.py": 1562771115138037410, - "venv/lib/python3.13/site-packages/pluggy/_hooks.py": 6941854999025808196, - "venv/lib/python3.13/site-packages/pip/_internal/operations/check.py": 16650663913976953800, - "venv/lib/python3.13/site-packages/_pytest/tmpdir.py": 10834220332037364219, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_datetimelike.py": 10294379314868108649, - "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.py": 5260748170237305627, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_arithmetic.py": 13304014482504911016, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_formats.py": 12299393760307251036, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate.py": 16723865897349422996, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/licenses/COPYING": 14459782230785388765, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py": 5908944827805063108, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/websockets/legacy/server.py": 5390090919193784186, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timedeltas.py": 1175883320586860493, - "venv/lib/python3.13/site-packages/fastapi/security/open_id_connect_url.py": 3502444317759870079, - "venv/lib/python3.13/site-packages/keyring/errors.py": 4587094338672775040, - "venv/lib/python3.13/site-packages/PIL/ImagePath.py": 18323293175183057101, - "venv/lib/python3.13/site-packages/PIL/FpxImagePlugin.py": 18245056799205041866, - "venv/lib/python3.13/site-packages/httpcore/_async/http_proxy.py": 13364803556177997526, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_api.py": 5287018911951723034, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_repeat.py": 11360772447747837958, - "venv/lib/python3.13/site-packages/fastapi/security/api_key.py": 6965937678104291169, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py": 7423946096970870727, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nditer.pyi": 33523064068376750, - "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.pyi": 5269136485429780038, - "venv/lib/python3.13/site-packages/pandas/core/computation/eval.py": 3176226283265954028, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/io/clipboard/__init__.py": 3656451154829206939, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ext.py": 8361831054662096695, - "venv/lib/python3.13/site-packages/pygments/styles/staroffice.py": 11694998942433220413, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__main__.py": 111505188798650285, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_complexcompat.h": 5001035774408914877, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/nested_schemas.py": 4114645644496880787, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test__exceptions.py": 12099001631178696189, - "venv/lib/python3.13/site-packages/pandas/_libs/writers.cpython-313-x86_64-linux-gnu.so": 6878855127769203372, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/mock.py": 14010776046610111182, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/tags.py": 254599446317461188, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_indexing.py": 1926721846569184112, - "venv/lib/python3.13/site-packages/pip/_vendor/pkg_resources/__init__.py": 1677532855362260338, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/licenses/LICENSE": 5669644120528099197, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pydantic/_internal/_namespace_utils.py": 5237766366340181382, - "venv/lib/python3.13/site-packages/pygments/styles/algol.py": 3040373291664740876, - "venv/lib/python3.13/site-packages/numpy/core/records.py": 12604652690721760437, - "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.pyi": 12275242654430570028, - "venv/bin/pyrsa-sign": 15564653706771700451, - "venv/lib/python3.13/site-packages/pygments/styles/solarized.py": 11231143047572616298, - "frontend/src/lib/components/assistant/__tests__/assistant_clarification.integration.test.js": 12258270009984504212, - "frontend/src/lib/components/reports/__tests__/reports_list.ux.test.js": 16252194679528846126, - "frontend/src/lib/components/reports/__tests__/fixtures/reports.fixtures.js": 10647302793388244094, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/numerictypes.pyi": 15565573472138344755, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/REQUESTED": 15130871412783076140, - "frontend/src/routes/translate/+page.svelte": 11304227812217935067, - "frontend/src/app.css": 3020720639702954139, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/timeout.py": 1807228452024740476, - "backend/src/plugins/translate/__tests__/test_token_budget.py": 9060506557524997011, - "backend/tests/test_smoke_app.py": 7819901244710945164, - "backend/alembic/versions/8dd0a93af539_drop_deprecated_source_language_column_.py": 3574830605384611294, - "venv/lib/python3.13/site-packages/annotated_types/test_cases.py": 5123395674810472016, - "backend/tests/core/test_migration_engine.py": 7419155921858935534, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/warnings.py": 2552021607611112605, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/entities.py": 8001287806688585081, - "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js": 16494645867501045726, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/setup.py": 17086801293303202021, - "venv/lib/python3.13/site-packages/httpcore/_async/socks_proxy.py": 9026788710898138090, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_timestamp.py": 12253372102602993034, - "venv/lib/python3.13/site-packages/pydantic/types.py": 785880062468926292, - "venv/lib/python3.13/site-packages/idna/__init__.py": 6489437464172324468, - "venv/lib/python3.13/site-packages/authlib/deprecate.py": 3563885283090710443, - "venv/lib/python3.13/site-packages/authlib/oauth1/client.py": 7314311571224771, - "venv/lib/python3.13/site-packages/_pytest/assertion/util.py": 16957445183087315124, - "venv/lib/python3.13/site-packages/pandas/api/types/__init__.py": 3995030354191071797, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg8000.py": 6580512354643849828, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress.py": 5951745251426856398, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/auth.py": 8455258275234626534, - "venv/lib/python3.13/site-packages/pip/_internal/utils/deprecation.py": 7435269308988385470, - "venv/lib/python3.13/site-packages/git/refs/remote.py": 4391983355222612205, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/spinner.py": 5564789578696016201, - "venv/lib/python3.13/site-packages/pycparser/plyparser.py": 5251740466238035035, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npy": 9694707264403109829, - "venv/lib/python3.13/site-packages/pandas/core/util/hashing.py": 13106575357604888913, - "venv/lib/python3.13/site-packages/dateutil/__init__.py": 17666544837260663800, - "backend/src/core/superset_client/_datasets_preview.py": 16476837380317508068, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/exclusions.py": 15852672500377116297, - "venv/lib/python3.13/site-packages/httpcore/_sync/http11.py": 5947339024588907032, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/METADATA": 5045041742469375466, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/keys.pyi": 1846673463183660140, - "venv/lib/python3.13/site-packages/pygments/styles/lovelace.py": 15082819162722685616, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/applicator": 6616723520993329538, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/claims.py": 11853156183819186599, - "venv/lib/python3.13/site-packages/h11/_connection.py": 6971907312737339559, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/token.py": 11655733747403312358, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/WHEEL": 12146818530001975731, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_loc.py": 2127584756555503873, - "venv/lib/python3.13/site-packages/rsa/pkcs1.py": 2576732594642491910, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpp": 4590574206187223887, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_non_compound.f90": 8048650253889213460, - "venv/lib/python3.13/site-packages/numpy/__config__.pyi": 1422114139050700402, - "venv/lib/python3.13/site-packages/ecdsa/rfc6979.py": 11564966030899162026, - "venv/lib/python3.13/site-packages/cryptography/hazmat/_oid.py": 755042927629281577, - "venv/lib/python3.13/site-packages/idna/compat.py": 9758194415584776667, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append.py": 13152738905565588661, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_astype.py": 16700461036658228009, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/dynamic.py": 15220116196072524330, - "backend/src/api/routes/git/_repo_routes.py": 7367823490321383813, - "venv/lib/python3.13/site-packages/pandas/_libs/hashing.cpython-313-x86_64-linux-gnu.so": 16124435225170610034, - "venv/lib/python3.13/site-packages/httpcore/_exceptions.py": 6716590711697088732, - "backend/src/models/git.py": 6898938895423263836, - "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.pyi": 2104100025130695295, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/ma/extras.pyi": 15292036493781556342, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunc_config.pyi": 2005481648919369399, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/box.py": 14303526978791498662, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel_py.py": 9112851333217812996, - "venv/lib/python3.13/site-packages/pandas/plotting/_misc.py": 10202078355301731720, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/hybrid.py": 14287665090144592501, - "venv/lib/python3.13/site-packages/pandas/compat/numpy/__init__.py": 1037113402100731082, - "venv/lib/python3.13/site-packages/pygments/scanner.py": 5640381644804445932, - "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.pyi": 1521384724035973688, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rsa/asn1.py": 3861968161559322784, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_convert.py": 13349243029437881949, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_setops.py": 16240008859650640717, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_categorical.py": 8844412630511751140, - "venv/lib/python3.13/site-packages/numpy/core/shape_base.py": 17925759824894973734, - "venv/lib/python3.13/site-packages/PIL/FitsImagePlugin.py": 618556384932056120, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/terminal_theme.py": 13727399800263579809, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/type_check.pyi": 9177256991985898824, - "venv/lib/python3.13/site-packages/pandas/core/sparse/__init__.py": 15130871412783076140, - "backend/src/core/plugin_loader.py": 245217431000027879, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/key_set.py": 8485970749081951741, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/gevent.py": 4811840397266785551, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_periodindex.py": 13430003945574795792, - "venv/lib/python3.13/site-packages/greenlet/TThreadStateCreator.hpp": 9887867766190459665, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_constructors.py": 9767900500541600486, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape_base.pyi": 10291383658131961346, - "venv/lib/python3.13/site-packages/anyio/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/PIL/ImagePalette.py": 9227837918026991107, - "venv/lib/python3.13/site-packages/PIL/ImageSequence.py": 16459886231802763032, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/misc.py": 6571241778569465264, - "frontend/src/lib/components/assistant/AssistantClarificationCard.svelte": 15872391111837089374, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tolist.py": 8343058000369672317, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_function.py": 7048185308307653546, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/tornado.py": 6491136785917169991, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/unitofwork.py": 3177255974394221645, - "venv/lib/python3.13/site-packages/pip/_internal/commands/cache.py": 1892313355296776523, - "frontend/build/_app/immutable/chunks/BAz4bHMQ.js": 13333198678504989936, - "venv/lib/python3.13/site-packages/jeepney/io/tests/conftest.py": 13381752264278264218, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/assertion.py": 5287436128235488464, - "venv/lib/python3.13/site-packages/numpy/_core/_rational_tests.cpython-313-x86_64-linux-gnu.so": 13475521165331256711, - "venv/lib/python3.13/site-packages/ecdsa/test_sha3.py": 12459459298663975730, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/claims.py": 14794441479740527265, - "venv/lib/python3.13/site-packages/pip/_internal/commands/completion.py": 4435231315212798769, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/__init__.py": 11121941644106061453, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_histograms.py": 47631283818191049, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexing.py": 9988098331562906950, - "venv/lib/python3.13/site-packages/PIL/_imagingmorph.pyi": 18222325750818585549, - "venv/lib/python3.13/site-packages/pygments/lexers/yang.py": 7409365668425173823, - "venv/lib/python3.13/site-packages/PIL/JpegPresets.py": 12342262642420923471, - "venv/lib/python3.13/site-packages/six.py": 948867418687428421, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npy": 17303060773267383537, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.APACHE": 11041304845352917971, - "venv/lib/python3.13/site-packages/greenlet/greenlet.h": 16216120538647882082, - "venv/lib/python3.13/site-packages/dotenv/version.py": 13508433229287636267, - "venv/lib/python3.13/site-packages/attr/filters.pyi": 13942974755090135724, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/astype_copy.pkl": 4470155972891419163, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_impl.py": 16610770935842519289, - "venv/lib/python3.13/site-packages/pandas/core/interchange/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/fastapi/dependencies/models.py": 8626894872248212015, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/__main__.py": 487896753130729554, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_categorical.py": 18254320571347851153, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py": 16571782049241458769, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_reductions.py": 8258332365408303156, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.py": 3654583356259850174, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling.py": 18221459425751198218, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/common.py": 10996276696597806957, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/strings.pyi": 15361498560917514616, - "venv/lib/python3.13/site-packages/psycopg2/errorcodes.py": 6711830404954314463, - "venv/lib/python3.13/site-packages/keyring/backends/null.py": 12740996416911604870, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/datonly.f90": 10495925225338237742, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_cpp_exception.py": 11851283355840567373, - "venv/lib/python3.13/site-packages/websockets/legacy/auth.py": 16685128086396181286, - "venv/lib/python3.13/site-packages/keyring/cli.py": 10996746519742056101, - "venv/lib/python3.13/site-packages/pluggy/_callers.py": 1042673447718196223, - "venv/lib/python3.13/site-packages/pillow.libs/libzstd-761a17b6.so.1.5.7": 17875236748140157032, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict_of_blocks.py": 11646835537993064324, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge.py": 11366935134260232329, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_store.py": 1500300322083708794, - "venv/lib/python3.13/site-packages/yaml/error.py": 5626374410579058436, - "venv/lib/python3.13/site-packages/yaml/parser.py": 18021832376874455801, - "venv/lib/python3.13/site-packages/starlette/__init__.py": 3252794766897205870, - "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.ux.test.js": 5439236328736424365, - "venv/lib/python3.13/site-packages/pandas/_libs/tslib.cpython-313-x86_64-linux-gnu.so": 10742615805788605480, - "venv/lib/python3.13/site-packages/git/__init__.py": 15867730798565725003, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_localize.py": 18409231586451494605, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_data.py": 2397937327057341, - "venv/lib/python3.13/site-packages/pydantic/_internal/_validate_call.py": 4078236486720616395, - "venv/lib/python3.13/site-packages/httpx/_models.py": 6516278895118988231, - "venv/lib/python3.13/site-packages/jose/backends/_asn1.py": 14123487576864105793, - "backend/src/services/reports/normalizer.py": 11408466232944555336, - "venv/lib/python3.13/site-packages/jsonschema_specifications/__init__.py": 5732822354953774657, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_converters.py": 8869440774594127419, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/session.py": 5126234270435358377, - "venv/lib/python3.13/site-packages/keyring/__init__.py": 991260689223167957, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_legendre.py": 18266595775500927868, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/manifest.py": 16998683210177680416, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.pyf": 14358606965464506398, - "venv/lib/python3.13/site-packages/pandas/_config/config.py": 1494288055210911451, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_round.py": 1299086932398824299, - "venv/lib/python3.13/site-packages/pydantic/v1/types.py": 10761779263335288559, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numerictypes.pyi": 15443101937711720707, - "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.pyi": 5786150108623563787, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/conftest.py": 3691712173202219606, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_reindex.py": 15794750188594767026, - "venv/lib/python3.13/site-packages/pip/_internal/commands/help.py": 4175797322922912326, - "venv/lib/python3.13/site-packages/pip/_internal/locations/base.py": 7966628150470073365, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_numeric.py": 9383693732009547935, - "venv/lib/python3.13/site-packages/pydantic/datetime_parse.py": 196119761382305574, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_apply.py": 7511915990531229598, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_compare.py": 3079569478684646491, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/_mapping.py": 11752660241391232497, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_unique.py": 3989602816674881215, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/RECORD": 15194367076254806710, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/METADATA": 6557531107125553027, - "venv/lib/python3.13/site-packages/packaging/_tokenizer.py": 7442392076059866359, - "frontend/src/components/backups/BackupManager.svelte": 15722995371746548624, - "backend/src/api/routes/__tests__/test_git_status_route.py": 6306531435114264696, - "venv/lib/python3.13/site-packages/pip/_internal/utils/entrypoints.py": 11268059714209889385, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/LICENSE": 2032205056284080825, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexers.py": 6773751569163287047, - "backend/src/plugins/search.py": 888817404897740233, - "venv/lib/python3.13/site-packages/pygments/lexers/_scilab_builtins.py": 17453812809700086888, - "frontend/src/routes/profile/__tests__/profile-preferences.integration.test.js": 6561454515118560091, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_cat.py": 15988884721768567895, - "venv/lib/python3.13/site-packages/pygments/formatters/_mapping.py": 13335227913146542481, - "logs/app.log.1": 15453131390328548426, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_subclass.py": 13490541871827928531, - "venv/lib/python3.13/site-packages/keyring/testing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/anyio/_core/_eventloop.py": 6853955946028131785, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/_typing/_dtype_like.py": 1598511035344858524, - "venv/lib/python3.13/site-packages/anyio/_backends/_trio.py": 4764555436057754985, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/padding.py": 11244071766694094554, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape.pyi": 2188930129299348535, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_assumed_shape.py": 18361009188431483164, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_object.py": 9215781825358309147, - "venv/lib/python3.13/site-packages/fastapi/testclient.py": 2606236077497278662, - "venv/lib/python3.13/site-packages/_pytest/doctest.py": 6126919596776592278, - "venv/lib/python3.13/site-packages/pydantic/_internal/_mock_val_ser.py": 12265320534388472158, - "venv/lib/python3.13/site-packages/numpy/_core/multiarray.pyi": 3494871767602648807, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/functions.py": 14107377669163193871, - "venv/lib/python3.13/site-packages/pandas/io/sql.py": 12592024569011796790, - "venv/lib/python3.13/site-packages/yaml/nodes.py": 3463622922914760653, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/anyio/_core/_signals.py": 1204322949639513552, - "venv/lib/python3.13/site-packages/pygments/formatters/__init__.py": 8009871891954370829, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_empty.py": 6845813091876023610, - "frontend/src/routes/dashboards/[id]/+page.svelte": 1225758611896418118, - "frontend/src/routes/settings/+page.svelte": 3103346197225933395, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptography.py": 4383854709657749855, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_subclassing.py": 17318983445227658412, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/bitwise_ops.py": 14350609278188982939, - "venv/lib/python3.13/site-packages/cffi/_cffi_errors.h": 1479575326813300292, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_fillna.py": 13675584998644907265, - "venv/lib/python3.13/site-packages/charset_normalizer/utils.py": 11724147997933584095, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_map.py": 10769974470610467636, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_string_array.py": 8260883866404398980, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_replace.py": 15992095120302643273, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py": 13869355993837330406, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_stack.py": 5316826578695144945, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_index_tricks.py": 4676293127859087991, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/lisp.py": 11872331622183590758, - "frontend/src/lib/components/health/HealthMatrix.svelte": 16085178486788467644, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.py": 1948707545068786, - "frontend/src/routes/+layout.svelte": 2748734957389681795, - "backend/src/core/__init__.py": 8673978627093182792, - "backend/src/models/mapping.py": 9358348320550260098, - "venv/lib/python3.13/site-packages/git/repo/fun.py": 9019364012746153312, - "venv/lib/python3.13/site-packages/pandas/_libs/algos.pyi": 22405377601029952, - "venv/lib/python3.13/site-packages/charset_normalizer/md.cpython-313-x86_64-linux-gnu.so": 1835299664481804968, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/base.py": 17992335652981439676, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_no_public_entities.f90": 12296500845667555672, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_reflection.py": 10520778598547003532, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_pickle.py": 3419420815386925630, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/PIL/PcdImagePlugin.py": 922809545463280224, - "venv/lib/python3.13/site-packages/numpy/ma/testutils.py": 9355402483340137461, - "venv/lib/python3.13/site-packages/more_itertools/recipes.pyi": 16788810745659802820, - "venv/lib/python3.13/site-packages/pygments/lexers/haskell.py": 8011341851439354724, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_can_hold_element.py": 5406431247116557833, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo.f": 5690596225005490325, - "venv/lib/python3.13/site-packages/jaraco/functools/__init__.py": 15334412919279565528, - "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.cpython-313-x86_64-linux-gnu.so": 17652852308919396403, - "venv/lib/python3.13/site-packages/charset_normalizer/version.py": 9948878594498587774, - "venv/lib/python3.13/site-packages/pandas/core/config_init.py": 16537122488227845090, - "venv/lib/python3.13/site-packages/pygments/lexers/pointless.py": 17552020039609007881, - "frontend/src/routes/login/+page.svelte": 4542360917085342323, - "backend/src/services/clean_release/repositories/approval_repository.py": 15069637305272665109, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pyodbc.py": 11271266527234292020, + "venv/lib/python3.13/site-packages/fastapi/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/WHEEL": 1598805043198466373, + "venv/lib/python3.13/site-packages/numpy/_core/cversions.py": 9724467539503410469, "venv/lib/python3.13/site-packages/numpy/core/numeric.py": 17025007229779320230, - "venv/lib/python3.13/site-packages/cffi/pkgconfig.py": 16057517851163939571, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/__init__.py": 13331622784664204066, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_base.py": 15292769510056833117, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_ccalendar.py": 8819357623337533489, - "venv/lib/python3.13/site-packages/pygments/lexers/eiffel.py": 8964563077187051946, - "venv/lib/python3.13/site-packages/pandas/io/formats/html.py": 11849941990375030601, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/modeline.py": 6194365452131561478, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.f": 1486878017876900890, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_pickle.py": 4216445955812104014, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_modules.py": 16835987029123241300, - "venv/bin/activate": 14735074000123383125, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/METADATA": 732041908491732594, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval.py": 8549680643865283913, - "venv/lib/python3.13/site-packages/pygments/lexers/nimrod.py": 8457135214131458774, - "venv/lib/python3.13/site-packages/tzlocal/windows_tz.py": 11696765495002954868, - "venv/lib/python3.13/site-packages/jsonschema/tests/__init__.py": 15130871412783076140, - "backend/src/models/dataset_review_pkg/_semantic_models.py": 15376906129805121935, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/live.py": 8586058300967410643, - "venv/lib/python3.13/site-packages/python_multipart/multipart.py": 5677735019292537174, - "backend/src/plugins/translate/__init__.py": 12400577052797809611, - "frontend/build/_app/immutable/nodes/33.BUdbGEpr.js": 7904426271120174786, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_elements_constructors.py": 9279447066034364952, - "venv/lib/python3.13/site-packages/tzlocal/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/starlette/middleware/httpsredirect.py": 16837082808103815044, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/status_codes.py": 17005213506461710202, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py": 15735839022111230173, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/abstract.py": 14216912708889351810, - "venv/lib/python3.13/site-packages/numpy/lib/npyio.py": 6782361985454523908, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_callback.py": 1689177048527329209, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_public_dtype_api_table.h": 8933016749764021181, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/__init__.py": 11283047029180207588, - "venv/lib/python3.13/site-packages/pandas/core/computation/check.py": 16048939716341154196, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/uts46data.py": 1867240043777383728, - "venv/lib/python3.13/site-packages/jeepney/io/tests/utils.py": 10346859893683522113, - "venv/lib/python3.13/site-packages/authlib/common/encoding.py": 52296066492753953, - "venv/lib/python3.13/site-packages/pillow.libs/libbrotlidec-b57ddf63.so.1.2.0": 10032590058372320813, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/recarray_from_file.fits": 15116318600514274152, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_api.py": 5813060676255093777, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/constructors.py": 7587476800569414769, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_algos.py": 5356470832688354593, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/json.py": 15085143436056608404, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/meson.build": 9642189398934130247, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/base.py": 16891437410462671317, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/claims.py": 12645788445743898136, - "frontend/build/_app/immutable/nodes/8.xxEF6bTJ.js": 5561886302919578979, - "venv/lib/python3.13/site-packages/pygments/formatters/svg.py": 11686316844190214147, - "backend/src/plugins/translate/__tests__/__init__.py": 2490542523242946173, - "venv/lib/python3.13/site-packages/pandas/core/indexes/period.py": 1181348384346239378, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.pyi": 15420142722808118826, - "venv/lib/python3.13/site-packages/iniconfig/exceptions.py": 16501270224069836264, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_melt.py": 15663807247682146566, - "venv/lib/python3.13/site-packages/pip/_internal/utils/filetypes.py": 4415340223268174913, - "venv/lib/python3.13/site-packages/greenlet/tests/test_leaks.py": 1219835696230215605, - "venv/lib/python3.13/site-packages/greenlet/tests/test_gc.py": 6383305344377882524, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_operators.py": 12686786829906210592, - "venv/lib/python3.13/site-packages/pip/_internal/pyproject.py": 6155748929233245415, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/loading.py": 3460142472461437916, - "venv/bin/keyring": 1488968652276547659, - "backend/src/services/llm_prompt_templates.py": 5706065356933042518, - "venv/lib/python3.13/site-packages/numpy/lib/__init__.py": 12211229657513986410, - "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators.py": 9928398334052202841, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_numeric.py": 9417128508433693658, - "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.py": 8786558894094307341, - "venv/lib/python3.13/site-packages/gitdb/test/test_stream.py": 18159319442241857857, - "venv/lib/python3.13/site-packages/pandas/tseries/frequencies.py": 2262381949755250516, - "backend/src/scripts/seed_superset_load_test.py": 14952302154142898180, - "frontend/build/_app/immutable/chunks/CTFFznDe.js": 8010860292122799172, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oidc/core/userinfo.py": 17201158389976619872, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE": 4274653168153720331, - "venv/lib/python3.13/site-packages/keyring/credentials.py": 14515496037842724455, - "venv/lib/python3.13/site-packages/pyasn1/codec/cer/encoder.py": 3261739474564948330, - "venv/lib/python3.13/site-packages/httpx/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_fillna.py": 7244491410004453166, - "frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js": 1250794495161202602, - "backend/src/services/clean_release/compliance_execution_service.py": 3379474749353250488, - "venv/lib/python3.13/site-packages/pygments/styles/trac.py": 13968346768164381833, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_round.py": 9358309072340573675, - "venv/lib/python3.13/site-packages/pyasn1/codec/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pydantic/utils.py": 1533455264689378477, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_internal/operations/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5support-d0bcff84.so.0.1": 16819924515291719993, - "venv/lib/python3.13/site-packages/pygments/lexers/_mapping.py": 12987629396101138833, - "venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py": 8159971229245441880, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_jsonschema_test_suite.py": 12733761462141495972, - "venv/lib/python3.13/site-packages/gitdb/exc.py": 13975852664048821278, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraysetops.pyi": 1782641795267171928, - "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/jws_eddsa.py": 833927568269372333, - "venv/lib/python3.13/site-packages/_pytest/_py/path.py": 8464061867075119556, - "venv/lib/python3.13/site-packages/click/__init__.py": 9261629899056870564, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_interaction.py": 12216477658994324787, - "venv/lib/python3.13/site-packages/passlib/handlers/sha1_crypt.py": 5752556581809938954, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reindex.py": 12459492712890114705, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_grouping.py": 5994909693214299971, - "venv/lib/python3.13/site-packages/fastapi/background.py": 16533963020573865222, - "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pxd": 14261507718442469988, - "venv/lib/python3.13/site-packages/pandas/core/arrays/period.py": 11560984428402318984, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/twodim_base.pyi": 3773329770170248877, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_ewm.py": 12153690217678546798, - "venv/lib/python3.13/site-packages/websockets/version.py": 5853118844880112885, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/memmap.pyi": 15629663272474054909, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_combine_concat.py": 3201068805128232483, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/characteristics.py": 2916810597149066609, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.py": 4111797647365870137, - "frontend/src/lib/components/dataset-review/ValidationFindingsPanel.svelte": 2403276296409515448, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_calendar.py": 12313641252472031631, - "backend/src/core/utils/async_network.py": 2763204398131017281, - "venv/lib/python3.13/site-packages/fastapi/security/utils.py": 2616633862993539558, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_recfunctions.py": 16014503284544747867, - "frontend/src/routes/reports/+page.svelte": 7370192460972346131, - "backend/src/core/logger.py": 2069537382060633888, - "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.pyi": 6534825471013879522, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py": 3042005457138956499, - "venv/lib/python3.13/site-packages/numpy/_core/_asarray.pyi": 3940937389249665490, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pipe.py": 7400355977900310233, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/engine.py": 2652248727360250985, - "venv/lib/python3.13/site-packages/websockets/sync/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/liblber-1c9097e2.so.2.0.200": 2651879839738900594, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77fixedform.f95": 10898637805043867370, - "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so": 2888939866336522308, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py": 10093932131797570986, - "venv/lib/python3.13/site-packages/PIL/ImtImagePlugin.py": 17599267755504817169, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_where.py": 3272402211717453881, - "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet.py": 10727595500186011102, - "venv/lib/python3.13/site-packages/numpy/core/fromnumeric.py": 2136433423902202138, - "venv/lib/python3.13/site-packages/pygments/formatters/rtf.py": 5850574956253747036, - "venv/lib/python3.13/site-packages/pygments/lexers/gsql.py": 15294109558028161389, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_values.py": 8372572954663628844, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_getitem.py": 18202683049446557047, - "venv/lib/python3.13/site-packages/pydantic/_internal/_internal_dataclass.py": 13206778611516171760, - "venv/lib/python3.13/site-packages/yaml/loader.py": 18284689412149643959, - "venv/lib/python3.13/site-packages/packaging/_manylinux.py": 8268418900652790584, - "venv/lib/python3.13/site-packages/packaging/licenses/__init__.py": 15879493360260891293, - "frontend/src/routes/translate/dictionaries/+page.svelte": 11204836397236633754, - "frontend/build/_app/immutable/chunks/C9KBnq-A.js": 13706048316691531557, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_mixed.py": 3476101699679108025, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh26681.f90": 1515497186927052106, - "venv/lib/python3.13/site-packages/attr/_cmp.pyi": 14443711863388606665, - "venv/lib/python3.13/site-packages/pip/_internal/commands/index.py": 16715578868094112894, - "venv/lib/python3.13/site-packages/pip/_internal/cli/command_context.py": 9426036980437931533, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_datetimeindex.py": 14816894350435545024, - "venv/lib/python3.13/site-packages/starlette/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/base.py": 10514193453447954780, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/repr.py": 839417422880281122, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_drop.py": 11341278355685583133, - "venv/lib/python3.13/site-packages/pip/_internal/cli/main_parser.py": 7942770969280622910, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatter.py": 16499277113779414101, - "venv/lib/python3.13/site-packages/sqlalchemy/exc.py": 6372950288664776625, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/base.py": 10205051944814316388, - "venv/lib/python3.13/site-packages/referencing/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/panel.py": 17832701246248000383, - "venv/lib/python3.13/site-packages/PIL/Hdf5StubImagePlugin.py": 15654153796225833950, - "venv/lib/python3.13/site-packages/pandas/core/window/ewm.py": 12143982379243011357, - "venv/lib/python3.13/site-packages/pandas/core/ops/__init__.py": 4049432353810047365, - "venv/lib/python3.13/site-packages/pygments/styles/perldoc.py": 4857683935715743356, - "venv/lib/python3.13/site-packages/pygments/styles/dracula.py": 12379322008538174078, - "venv/lib/python3.13/site-packages/pygments/lexers/jslt.py": 11805394130669644345, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hiworld.f90": 6321872653169140461, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_argparse.py": 11111699418217357084, - "venv/lib/python3.13/site-packages/pandas/_libs/ops.pyi": 1192312467209548763, - "venv/lib/python3.13/site-packages/numpy/_core/printoptions.pyi": 16758392990920041462, - "frontend/src/app.html": 10207534912546549080, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets_properties.py": 2340780879864207500, - "frontend/src/lib/Counter.svelte": 4002153662176313030, - "frontend/build/_app/immutable/chunks/B19Q4MUf.js": 8384959676363720729, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_pickle.py": 2910383574326146698, - "backend/src/services/clean_release/__init__.py": 18311663871116449547, - "venv/lib/python3.13/site-packages/anyio/streams/tls.py": 4191898645554875562, - "backend/alembic/script.py.mako": 7041141123739810216, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_django.py": 8455054295577203929, - "venv/lib/python3.13/site-packages/PIL/TarIO.py": 13651401276968618561, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py": 15077617224435782028, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_digest.py": 7988469818593853943, - "venv/lib/python3.13/site-packages/git/objects/util.py": 691018806932355307, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data": 9599129816090399315, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_ordered.py": 13827016781743929164, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_util.py": 16024147572807865219, - "frontend/build/_app/immutable/chunks/D7OGuSC7.js": 3496948723181364522, - "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.py": 10803011906582324302, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/__init__.py": 12793458753561114666, - "venv/lib/python3.13/site-packages/pydantic_core/core_schema.py": 8106298745123589494, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/strategies.py": 14007486769065345085, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/setitem.py": 9552868454186447925, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata.py": 2021612995853910808, - "venv/lib/python3.13/site-packages/_pytest/subtests.py": 14573790885130724739, - "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.pyi": 9854069702366900663, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isna.py": 18394053826166267096, - "venv/lib/python3.13/site-packages/pygments/lexers/_lua_builtins.py": 12653747099659257203, - "venv/lib/python3.13/site-packages/passlib/utils/decor.py": 21351391767102899, - "venv/lib/python3.13/site-packages/passlib/tests/sample1.cfg": 9597283806285213340, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/common_with_division.f": 13517753924061311074, - "venv/lib/python3.13/site-packages/passlib/crypto/__init__.py": 16409196868630049365, - "venv/lib/python3.13/site-packages/sqlalchemy/util/langhelpers.py": 1246582552828542841, - "venv/lib/python3.13/site-packages/pip/_internal/commands/search.py": 8292218231992728142, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/LICENSE": 16174281248426886206, - "venv/lib/python3.13/site-packages/passlib/totp.py": 14590474309565233318, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_to_timestamp.py": 17572983799929378095, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/WHEEL": 8954358347596196608, - "venv/lib/python3.13/site-packages/gitdb/test/test_base.py": 16305779366785148795, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_utils.py": 8668650648802195016, - "venv/lib/python3.13/site-packages/pydantic/experimental/arguments_schema.py": 8644805798814234457, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/content": 12465123883296161860, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_function_base.pyi": 10972502518588022706, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_ops.py": 14420421526781215500, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/__init__.py": 15991623390785635773, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/fetch.py": 13937019222549208796, - "venv/lib/python3.13/site-packages/jsonschema/tests/typing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py": 106471890743211548, - "venv/lib/python3.13/site-packages/passlib/handlers/__init__.py": 3424087230285514388, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_as_unit.py": 4283520188709399244, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.pyi": 2700568594438441246, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_dict.py": 16252265723192124609, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/common/pythoncapi-compat/COPYING": 3474104874623144155, - "frontend/build/_app/immutable/chunks/DcQow1yy.js": 691404182357373926, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/base.py": 16052592982063844259, - "frontend/build/_app/immutable/assets/0.DN6ByAb-.css": 6256222992586029103, - "venv/lib/python3.13/site-packages/pygments/lexers/praat.py": 54226477748516386, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/base.py": 6348888156686452825, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix_py.py": 6653895521961110080, - "venv/lib/python3.13/site-packages/anyio/_core/_contextmanagers.py": 1258491775823080427, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_frozen.py": 3484963987053884566, - "venv/lib/python3.13/site-packages/pydantic/v1/mypy.py": 17076910404409672245, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsin.csv": 1665171675551692225, - "venv/lib/python3.13/site-packages/pydantic/schema.py": 83238111330132033, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_pickle.py": 8705060420848529055, - "venv/lib/python3.13/site-packages/pygments/lexers/x10.py": 6167378515780115912, - "venv/lib/python3.13/site-packages/_pytest/stash.py": 14430252294763173610, - "venv/lib/python3.13/site-packages/pandas/tests/test_multilevel.py": 10500316376521962061, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_py_util.py": 12836989066113400538, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/discovery.py": 12479996594187326798, - "frontend/src/routes/dashboards/[id]/components/DashboardTaskHistory.svelte": 8800115118935646769, - "venv/lib/python3.13/site-packages/pandas/core/methods/describe.py": 17006836623161521092, - "backend/logs/app.log.4": 11598841362861329809, - "venv/lib/python3.13/site-packages/passlib/handlers/ldap_digests.py": 8658529184965699766, - "venv/lib/python3.13/site-packages/pandas/_libs/ops.cpython-313-x86_64-linux-gnu.so": 10490009128553397250, - "venv/lib/python3.13/site-packages/pygments/lexers/functional.py": 15896849233007178031, - "venv/lib/python3.13/site-packages/pandas/compat/pyarrow.py": 13693379998785127538, - "backend/src/api/routes/dashboards/_projection.py": 1559143255780155174, - "backend/src/services/clean_release/__tests__/test_manifest_builder.py": 9320699981861675127, - "backend/src/services/git/__init__.py": 14011752637167919253, - "backend/src/plugins/llm_analysis/__tests__/test_client_headers.py": 5527172390260032966, - "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_gather.py": 3634138701088855517, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/emath.pyi": 3718540478840842664, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_csv.py": 10737444103906389448, - "frontend/src/lib/components/assistant/__tests__/assistant_confirmation.integration.test.js": 12582020574293031526, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_npy_units.py": 8966226962799105383, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_repeat.py": 9155935990114743583, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_applicator_schemas.py": 8395157526634674143, - "frontend/src/lib/components/dataset-review/__tests__/us2_semantic_workspace.ux.test.js": 3550956860258259058, - "venv/lib/python3.13/site-packages/ecdsa/test_pyecdsa.py": 14542286382742044648, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_inspect.py": 10573558345297891848, - "venv/lib/python3.13/site-packages/apscheduler/executors/asyncio.py": 2230610508870117058, - "backend/src/api/routes/profile.py": 16065196073183170065, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.pyi": 1830368009811266890, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayprint.pyi": 2053388407376896377, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_head_tail.py": 1906121476322841736, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/__init__.py": 15130871412783076140, - "frontend/build/_app/immutable/nodes/27.BcmdCO7Q.js": 17863709917454568143, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs12.pyi": 7801211423226951223, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/METADATA": 5628172948035858892, - "frontend/build/index.html": 12181298379271754142, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/json.py": 6429651405841289922, - "venv/lib/python3.13/site-packages/numpy/matlib.pyi": 8017902013455539981, - "venv/lib/python3.13/site-packages/passlib/handlers/postgres.py": 14162382430051693313, - "venv/lib/python3.13/site-packages/httpcore/_trace.py": 9576253018891841832, - "venv/lib/python3.13/site-packages/rsa/randnum.py": 13958355153634534526, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_size.py": 12449835577388069407, - "venv/lib/python3.13/site-packages/bcrypt/__about__.py": 11196369962175692786, - "backend/tests/services/clean_release/test_approval_service.py": 8359796700245201345, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_intervalindex.py": 5053113227747333759, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/base.py": 14908084548919567658, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/version.py": 14723293207188512766, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ec.py": 8864112842559211971, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/METADATA": 9586902338916246692, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/plugin.py": 18006138132596504101, - "venv/lib/python3.13/site-packages/pip/__init__.py": 9449893869469432503, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/resolution.py": 5132658725516243311, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/__init__.py": 8924316361489544973, - "venv/lib/python3.13/site-packages/pydantic/v1/json.py": 16596969681914729251, - "venv/lib/python3.13/site-packages/urllib3/poolmanager.py": 15431313180245757657, - "backend/src/core/superset_client/_base.py": 17832258524611728315, - "venv/lib/python3.13/site-packages/_pytest/pastebin.py": 13946273207624603063, - "venv/lib/python3.13/site-packages/smmap/buf.py": 18231775424238352734, - "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.pyi": 1942236362568669510, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_copy_deprecation.py": 16045426957636349035, - "venv/lib/python3.13/site-packages/numpy/_array_api_info.py": 9709101823494959741, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_find_common_type.py": 9581516171321396333, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/emoji.py": 9166774203918731896, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop.py": 15751236355035730554, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_monotonic.py": 4626065848596214108, - "venv/lib/python3.13/site-packages/sqlalchemy/future/engine.py": 4639404437703898767, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_select.py": 17823338162428379468, - "venv/lib/python3.13/site-packages/pygments/lexers/perl.py": 17014528337020572681, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_ios.h": 6751259472945638998, - "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.pyi": 9935587108964614958, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/zip-safe": 15240312484046875203, - "backend/src/api/routes/git/_merge_routes.py": 2019590524299108754, - "backend/src/api/routes/translate/_run_list_routes.py": 18118346695557887371, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/h11_impl.py": 1419719055394402145, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/specifiers.py": 12284901056355482991, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_strings.py": 7379299916213794543, - "backend/src/core/logger/__tests__/test_logger.py": 4188397720199242180, - "venv/lib/python3.13/site-packages/pygments/styles/_mapping.py": 11752660241391232497, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/METADATA": 6656194161182818439, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_array.py": 12514884344165043136, - "backend/src/services/clean_release/__tests__/test_report_builder.py": 17265459530244848426, - "frontend/src/lib/components/translate/TranslationMetricsDashboard.svelte": 10832255927386244176, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_libalgos.py": 13934861984871432400, - "backend/src/models/__init__.py": 13280325675752905975, - "frontend/src/routes/tools/mapper/+page.svelte": 11954517841045690204, - "frontend/src/lib/api.js": 12667793754050845906, - "venv/lib/python3.13/site-packages/keyring/backends/macOS/__init__.py": 1010937986753403630, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadb.py": 3109042476851791996, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/associationproxy.py": 7421910137064160936, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_function_base.py": 16624804022675533740, - "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.pyi": 9243829615414967057, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pxd": 13413785818999584986, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_file.py": 5180401483285462901, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/auto.py": 12818217385551437286, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py": 10843527833285941674, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_get_dummies.py": 14871264482622327003, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/relationships.py": 8401569216905291119, - "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.py": 12251264586530536622, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_setitem.py": 10389770446834773837, - "venv/lib/python3.13/site-packages/sqlalchemy/util/tool_support.py": 4926732415520779988, - "venv/lib/python3.13/site-packages/anyio/_core/_asyncio_selector_thread.py": 1760597191437520000, - "venv/lib/python3.13/site-packages/_pytest/mark/expression.py": 1821437025841201912, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string.py": 3834067244989779682, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress_bar.py": 15595912766961891283, - "venv/lib/python3.13/site-packages/pydantic/color.py": 1007422551673129827, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-2.csv": 14641856424618845798, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_impl.py": 15673691977157706075, - "venv/lib/python3.13/site-packages/websockets/asyncio/client.py": 5694225754072131767, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_insert.py": 17383986803728810587, - "venv/lib/python3.13/site-packages/pygments/lexers/roboconf.py": 1335271183806115470, - "backend/src/core/utils/superset_context_extractor/_filters.py": 281666313889333545, - "venv/lib/python3.13/site-packages/_pytest/_version.py": 4890545974654141536, - "venv/lib/python3.13/site-packages/pydantic/v1/schema.py": 10008526732895436361, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_retain_attributes.py": 16210262356844956764, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/oct_key.py": 11504245602045724, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_sorting.py": 8944017570338511677, - "frontend/src/assets/svelte.svg": 13872699371039797074, - "venv/lib/python3.13/site-packages/numpy/random/_mt19937.pyi": 15904463104068500379, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex_like.py": 13803156447991282315, - "venv/lib/python3.13/site-packages/pip/_internal/req/__init__.py": 12266077985828139928, - "docker/nginx.conf": 16884137323072604795, - "venv/lib/python3.13/site-packages/pandas/core/arrays/integer.py": 7775055011080964709, - "venv/lib/python3.13/site-packages/pydantic/v1/errors.py": 11245135449608452720, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING": 14459782230785388765, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_cython.py": 2933400021016349431, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_frame.py": 9378623452601244234, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/compat.py": 5871636909854554063, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/string.tpl": 15796203549130590784, - "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/array.py": 1919242530821725197, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/cache_key.py": 11118819065204404106, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.py": 5583478355093966990, - "venv/lib/python3.13/site-packages/pandas/tests/extension/date/__init__.py": 16789767433439616593, - "venv/lib/python3.13/site-packages/jeepney/io/common.py": 15160647397460834508, - "venv/lib/python3.13/site-packages/PIL/ImageStat.py": 18442709639217312689, - "frontend/src/services/adminService.js": 6375990585512014277, - "backend/src/plugins/translate/dictionary.py": 5036394504674042587, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odswriter.py": 5134385709728943904, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py": 14810883299259169406, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/PIL/TiffTags.py": 8688883943071546469, - "frontend/build/_app/immutable/chunks/CUbMcWr2.js": 11521095389159805757, - "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.py": 14034239842770171660, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/conftest.py": 17514079438846557943, - "frontend/src/routes/dashboards/__tests__/dashboard-profile-override.integration.test.js": 15198772446275187033, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_inference.py": 5741637509343419942, - "backend/src/core/utils/superset_context_extractor/_templates.py": 6790136732701012567, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.cpython-313-x86_64-linux-gnu.so": 14468234464367691795, - "venv/lib/python3.13/site-packages/PIL/_webp.pyi": 18222325750818585549, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_py.py": 6428078990080243608, - "venv/lib/python3.13/site-packages/jsonschema/_typing.py": 13152346021901861822, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/__init__.py": 371829636800613137, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/RECORD": 18314788057673509075, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__init__.py": 8880435562116660648, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_asof.py": 15792528637923603240, - "venv/lib/python3.13/site-packages/pydantic/_internal/_config.py": 12760390602912369447, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pymssql.py": 6650008663791598065, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_period.py": 10309599277099605395, - "venv/lib/python3.13/site-packages/pandas/tests/test_take.py": 17628004130770326677, - "venv/lib/python3.13/site-packages/pygments/lexers/tnt.py": 4657130609241407766, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_accumulations.py": 11428556645065711062, - "venv/lib/python3.13/site-packages/greenlet/tests/test_tracing.py": 10227316489696807112, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_indexing.py": 1947969642791009285, - "venv/lib/python3.13/site-packages/PIL/XbmImagePlugin.py": 12256750478268246137, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunclike.py": 12077553320554633586, - "venv/lib/python3.13/site-packages/_pytest/timing.py": 14080119171718125986, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/client_credentials.py": 256154392957334831, - "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.pyi": 2029639875417260268, - "venv/lib/python3.13/site-packages/jsonschema/_types.py": 9250091433305395923, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/errors.py": 14634768861119075506, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py": 2516476609539242190, - "venv/lib/python3.13/site-packages/pandas/tests/window/conftest.py": 10673264126684571493, - "venv/lib/python3.13/site-packages/pygments/lexers/_qlik_builtins.py": 1728058904137960444, - "venv/lib/python3.13/site-packages/dateutil/tzwin.py": 5149446703746204349, - "frontend/src/routes/dashboards/[id]/components/DashboardLinkedResources.svelte": 8279080994517688482, - "backend/src/dependencies.py": 9481683691730170837, - "venv/lib/python3.13/site-packages/websockets/asyncio/compatibility.py": 9716993942839407921, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/style.py": 10665998898253460114, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_warnings.py": 13071210233876253749, - "venv/lib/python3.13/site-packages/PIL/PsdImagePlugin.py": 4347581352716313298, - "venv/lib/python3.13/site-packages/pandas/_testing/_io.py": 14264259716673715185, - "venv/bin/uvicorn": 3776670267111625708, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/WHEEL": 5160964596297665692, - "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.py": 893743087939984792, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_dialect.py": 6294522482440261756, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_function.py": 14142346897145169685, - "venv/lib/python3.13/site-packages/charset_normalizer/constant.py": 3070316197495651997, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/authorization_server.py": 17967809814827547139, - "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.py": 11393954913234866059, - "frontend/build/_app/immutable/nodes/31.BSHS4QPz.js": 2044128076272545863, - "backend/src/core/ws_log_handler.py": 7507069843556560249, - "venv/lib/python3.13/site-packages/keyring/backends/chainer.py": 12526770797516553385, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_dtypes.py": 481191723198819568, - "venv/lib/python3.13/site-packages/smmap/test/test_tutorial.py": 16309370344201290582, - "frontend/src/routes/git/+page.svelte": 14169430923974356254, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/concat.py": 14737724508458849795, - "venv/lib/python3.13/site-packages/pandas/tests/series/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/METADATA": 7214286447912789067, - "backend/src/api/routes/__tests__/test_connections_routes.py": 18044815559820610056, - "venv/lib/python3.13/site-packages/starlette/middleware/sessions.py": 11719848399661632707, - "venv/lib/python3.13/site-packages/pygments/lexers/lean.py": 14518351619544450286, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_slp_switch.py": 16490710972475154509, - "frontend/src/components/tools/ConnectionForm.svelte": 4060811448046342875, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/poolmanager.py": 5071697837617938064, - "venv/lib/python3.13/site-packages/websockets/legacy/handshake.py": 376095144657994920, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pygments/styles/native.py": 4559566444883341527, - "venv/lib/python3.13/site-packages/anyio/streams/buffered.py": 8916188172110578587, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/emscripten_fetch_worker.js": 10530710876604536879, - "venv/lib/python3.13/site-packages/uvicorn/lifespan/on.py": 2866195356090164296, - "venv/lib/python3.13/site-packages/pygments/lexers/_stan_builtins.py": 1961875544235948177, - "venv/lib/python3.13/site-packages/_pytest/_io/terminalwriter.py": 16852767249156780980, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_astype.py": 6578539240745332772, - "venv/lib/python3.13/site-packages/greenlet/TMainGreenlet.cpp": 9489520001674089404, - "venv/lib/python3.13/site-packages/websockets/imports.py": 1995356703044158461, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_values.py": 430620320423334461, - "venv/lib/python3.13/site-packages/pycparser/c_ast.py": 5942107079119742, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/type_check.pyi": 1599910480066368464, - "frontend/src/lib/components/reports/__tests__/reports_filter_performance.test.js": 10583581505638859039, "venv/lib/python3.13/site-packages/passlib/tests/test_context.py": 15195609282700801214, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_lexsort.py": 10394832796613968484, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-1.csv": 9129730764673472086, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ctypeslib.pyi": 8623725566434725526, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_duplicate_labels.py": 10663105616336167414, - "venv/lib/python3.13/site-packages/httpx/_transports/mock.py": 13564349939188292363, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_algs.py": 2479791018067637690, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.pyi": 3814281717779245045, - "venv/lib/python3.13/site-packages/passlib/utils/binary.py": 13123975436674239306, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/getitem.py": 556339939822508169, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlsxwriter.py": 8445547874832015203, - "venv/lib/python3.13/site-packages/PIL/EpsImagePlugin.py": 1824020964179364491, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/licenses/LICENSE": 1126906383149772681, - "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.pyi": 15864712173815045309, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/RECORD": 7914594107698271488, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/scalar_string.f90": 15018631706178918578, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/LICENSE": 6989336036499641077, - "frontend/build/_app/immutable/nodes/12.B-jKzEjW.js": 7311098820038156560, - "venv/lib/python3.13/site-packages/PIL/TgaImagePlugin.py": 9208942494595222259, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tz_localize.py": 13699016868476153393, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi": 18143251582901996072, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.cpython-313-x86_64-linux-gnu.so": 15960606468596245469, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cython.py": 14695721638531081573, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_timedelta64.py": 16821719402662529276, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_copy.py": 12902342280706372953, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_arithmetic.py": 976970179757115407, - "venv/lib/python3.13/site-packages/pygments/lexers/graphics.py": 13783143864729682444, - "venv/lib/python3.13/site-packages/pip/_internal/exceptions.py": 2252826732781519645, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.pyi": 9003794318682878317, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api1.c": 8494977275215578998, - "venv/lib/python3.13/site-packages/certifi/py.typed": 15130871412783076140, - "frontend/src/lib/components/ui/MultiSelect.svelte": 13967450784969599105, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/device_code.py": 6712610002227849281, - "backend/src/core/utils/superset_context_extractor/__init__.py": 1074191108198498747, - "backend/src/core/utils/superset_compilation_adapter.py": 4974836712746468095, - "backend/src/services/clean_release/audit_service.py": 9534298777117548502, - "backend/src/plugins/translate/orchestrator.py": 1317097146823609899, - "venv/lib/python3.13/site-packages/pygments/lexers/elpi.py": 5736689911777977822, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_unary.py": 7128845965976836334, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_endian.h": 2761502363895204652, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_astype.py": 10171766562093740708, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_rolling.py": 11118160360072677569, + "venv/lib/python3.13/site-packages/pygments/lexers/data.py": 9365024327689738663, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.py": 15511055314565873928, + "backend/src/services/clean_release/preparation_service.py": 10596397721803950912, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_transpose.py": 17983160984979323393, + "venv/lib/python3.13/site-packages/keyring/cli.py": 10996746519742056101, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_drop_duplicates.py": 12601598844977346213, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.cpython-313-x86_64-linux-gnu.so": 6011605826065363427, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/LICENSE": 6774760218010069963, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_getlimits.py": 118041882543413669, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/__init__.py": 11850945878149145990, + "frontend/build/_app/immutable/chunks/BIIQCMQD.js": 10069504695005052902, + "venv/lib/python3.13/site-packages/passlib/tests/sample1b.cfg": 2872366861156549016, + "docker/backend.Dockerfile": 10320070102635663967, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5support-d0bcff84.so.0.1": 16819924515291719993, + "venv/lib/python3.13/site-packages/pandas/compat/pyarrow.py": 13693379998785127538, + "venv/lib/python3.13/site-packages/pandas/_libs/pandas_parser.cpython-313-x86_64-linux-gnu.so": 18056339336364727359, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/parameters.py": 247312908510013930, + "backend/src/services/health_service.py": 7295235621662158359, + "venv/lib/python3.13/site-packages/packaging/__init__.py": 5479277484104486860, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/context.py": 1041624619197012753, + "venv/lib/python3.13/site-packages/PIL/McIdasImagePlugin.py": 14137545069906812251, + "backend/src/services/clean_release/stages/manifest_consistency.py": 14323717074129560178, + "venv/lib/python3.13/site-packages/yaml/composer.py": 13619070314468518566, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.obj": 2884737706935637437, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/METADATA": 5628172948035858892, + "backend/src/core/task_manager/context.py": 17543992255093146942, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/tools.py": 6437315459200419827, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/__init__.py": 1398617034174236358, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py": 12245524932757110911, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_join.py": 11910166566689889389, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/errors.py": 4817746830102488448, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py": 11422235699126032386, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/METADATA": 11439969143853665311, + "venv/lib/python3.13/site-packages/git/objects/submodule/root.py": 5322248117564025370, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.py": 6617116180936986481, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py": 1551768441102117466, + "venv/lib/python3.13/site-packages/pycparser/plyparser.py": 5251740466238035035, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937_regressions.py": 8981142329261965538, + "venv/lib/python3.13/site-packages/anyio/lowlevel.py": 11919467357746648592, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/multiarray.pyi": 6368268452184570195, + "venv/lib/python3.13/site-packages/jose/backends/rsa_backend.py": 13507186353881931758, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/__init__.py": 11283047029180207588, + "venv/lib/python3.13/site-packages/websockets/sync/connection.py": 2791040669094103200, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py": 9931394693284776391, + "venv/lib/python3.13/site-packages/numpy/core/arrayprint.py": 9652368112174245943, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.pyi": 2884703536776830012, "venv/lib/python3.13/site-packages/pandas/tests/window/test_timeseries_window.py": 1973365560758236222, - "venv/lib/python3.13/site-packages/pygments/lexers/sophia.py": 15971500155124949058, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_formats.py": 12299393760307251036, + "venv/lib/python3.13/site-packages/pygments/lexers/typoscript.py": 6706632522710582762, + "backend/src/models/translate.py": 1714500577896249923, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_describe.py": 11923715539897559626, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/_orm_constructors.py": 4276726508722638048, + "backend/src/services/clean_release/artifact_catalog_loader.py": 7672031618376179570, + "venv/lib/python3.13/site-packages/greenlet/tests/test_tracing.py": 10227316489696807112, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/constants.pyi": 15633065380510755171, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numeric.py": 14451517189908259173, + "backend/src/core/migration/archive_parser.py": 3022511117854063904, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polybase.pyi": 3404315099794022355, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/modes.py": 6832635056228445650, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/licenses/LICENSE": 8364608327598414179, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hiworld.f90": 6321872653169140461, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_pytables_missing.py": 12154582133989896069, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming_py.py": 16133454512601432815, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/apply.py": 3906017176873949738, + "venv/lib/python3.13/site-packages/PIL/ImImagePlugin.py": 15452956038794399492, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_level_values.py": 12490886203425393290, + "backend/src/services/__tests__/test_health_service.py": 5498183976797488518, + "venv/lib/python3.13/site-packages/pandas/tests/apply/common.py": 1113935558495455492, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/RECORD": 9461427699726982537, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/implicit.py": 7379110378465568776, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.cpython-313-x86_64-linux-gnu.so": 2311012065007769341, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_to_offset.py": 3929599794655343911, + "venv/lib/python3.13/site-packages/git/util.py": 7424113599506269603, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/proxy.py": 13008557041076030198, + "venv/lib/python3.13/site-packages/pydantic/_internal/_forward_ref.py": 10623110912945011703, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polyutils.py": 675639791972685455, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/named_types.py": 6099185243355915896, + "venv/lib/python3.13/site-packages/pygments/lexers/resource.py": 8057351545636349791, + "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js": 16494645867501045726, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_asof.py": 18383318159051131669, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/char.f90": 12203871998988759364, + "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp.py": 16797760903996739079, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/__init__.py": 3673322722277252961, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get_value.py": 389530025760539462, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_quoting.py": 1147992698412929658, + "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.test.js": 7689381541193660594, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/pip/_internal/locations/base.py": 7966628150470073365, + "backend/src/core/task_manager/__init__.py": 15991802770215124472, + "venv/lib/python3.13/site-packages/greenlet/PyModule.cpp": 3464405102256525511, + "venv/lib/python3.13/site-packages/PIL/MspImagePlugin.py": 3034490726971829439, + "venv/lib/python3.13/site-packages/passlib/handlers/sun_md5_crypt.py": 1949993216707182407, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix_py.py": 17041764258638228993, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py": 14619529283550788275, + "venv/lib/python3.13/site-packages/sqlalchemy/exc.py": 6372950288664776625, + "venv/lib/python3.13/site-packages/pygments/formatters/irc.py": 2133711525226123259, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ec.py": 8864112842559211971, + "frontend/src/components/git/GitMergeDialog.svelte": 17029360862035548928, + "backend/src/services/dataset_review/__init__.py": 15781509495698823404, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/scoping.py": 17216492046645992674, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/__init__.py": 10920760520224795596, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexers.py": 6773751569163287047, + "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.py": 9654828595008925976, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_old_ma.py": 18116460138221458026, + "backend/git_repos/remote/test-repo.git/info/exclude": 6403833611826357791, + "venv/lib/python3.13/site-packages/starlette/middleware/httpsredirect.py": 16837082808103815044, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/models.py": 11521700876708601977, + "venv/lib/python3.13/site-packages/authlib/oidc/core/errors.py": 18154419146257747681, + "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.py": 13115032674230220791, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_arithmetics.py": 16527169124402715210, + "venv/lib/python3.13/site-packages/cryptography/x509/__init__.py": 4066756344049307453, + "backend/src/scripts/create_admin.py": 16927424133554976030, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/concatkdf.py": 6712732248328458871, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/publicmod.f90": 2174121054736811903, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_cumulative.py": 15644124412739892167, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/asyncio.py": 4200928269459447593, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows.py": 13341168721223961872, + "venv/lib/python3.13/site-packages/anyio/abc/__init__.py": 11428476337116642278, + "venv/lib/python3.13/site-packages/pygments/lexers/supercollider.py": 15306357945850389082, + "frontend/build/favicon.svg": 6451919037497541980, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_pickle.py": 388513670243918273, + "frontend/src/lib/components/reports/reportTypeProfiles.js": 10752964062056890725, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pop.py": 5700439923973346681, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL": 18203500019759199992, + "frontend/build/_app/immutable/nodes/37.BSv7824F.js": 6049574657155009311, + "backend/src/core/utils/superset_context_extractor/_pii.py": 14826864290194304173, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_fillna.py": 7244491410004453166, + "backend/src/services/clean_release/repository.py": 11403333156347178483, + "backend/git_repos/remote/test-repo.git/objects/8c/4ea7958b7a7e140d27e1e7d8d1f9912fdf5992": 5862166356451798197, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs7.py": 9623344347519683527, + "venv/lib/python3.13/site-packages/numpy/tests/test_lazyloading.py": 17465787495701737655, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_select.py": 7083040959665006634, + "venv/lib/python3.13/site-packages/pandas/_libs/window/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/git/index/util.py": 5022401275553420722, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.pyi": 5973671421648618563, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_value_counts.py": 12340872812120847127, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.h": 4516275133897904047, + "venv/lib/python3.13/site-packages/numpy/ma/extras.pyi": 15292036493781556342, + "venv/lib/python3.13/site-packages/git/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayscalars.h": 16142050006935459679, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/indexable.py": 4131796922597075758, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiarray.py": 5928288171180750841, + "venv/lib/python3.13/site-packages/pandas/util/_decorators.py": 14780166101442507665, + "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.pyi": 16176428602589420913, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/memory.py": 9947113720821563254, + "venv/lib/python3.13/site-packages/rapidfuzz/utils_py.py": 12341806772632276946, + "venv/lib/python3.13/site-packages/PIL/TiffTags.py": 8688883943071546469, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/METADATA": 3390257212824996989, + "venv/lib/python3.13/site-packages/pygments/lexers/cplint.py": 13990118342267278047, + "backend/src/core/superset_profile_lookup.py": 5518507735632088337, + "venv/lib/python3.13/site-packages/starlette/testclient.py": 16088848330558215194, + "venv/lib/python3.13/site-packages/requests/__version__.py": 6677532709758546816, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/base.py": 10514193453447954780, + "venv/lib/python3.13/site-packages/pydantic/v1/types.py": 10761779263335288559, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/__init__.py": 12849371269359608058, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/pbkdf2.py": 3369246520501764730, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_datetimelike.py": 17304541521108700317, "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token.py": 4747545058753492487, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.py": 5735043585893007971, + "venv/lib/python3.13/site-packages/pygments/lexers/ride.py": 14096748896451437594, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/LICENSE": 14599279238716790189, + "venv/lib/python3.13/site-packages/pygments/lexers/agile.py": 5686162551416231457, + "frontend/build/_app/immutable/chunks/D9dVwiHl.js": 14131011399955207172, + "backend/src/api/__init__.py": 5287505122015708040, + "venv/lib/python3.13/site-packages/uvicorn/logging.py": 12858188863836778374, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/nditer.py": 10038360623032425884, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/METADATA": 6656194161182818439, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_api.py": 5287018911951723034, + "frontend/src/lib/stores/__tests__/assistantChat.test.js": 11822882112255142537, + "venv/lib/python3.13/site-packages/starlette/applications.py": 13591448461234020757, + "venv/lib/python3.13/site-packages/pandas/_libs/hashing.cpython-313-x86_64-linux-gnu.so": 16124435225170610034, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_validate.py": 12789481642730855139, + "backend/git_repos/remote/test-repo.git/hooks/push-to-checkout.sample": 4825338927654285910, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/x963kdf.py": 14995426099932276090, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/iniconfig/py.typed": 15130871412783076140, + "backend/src/models/__tests__/test_models.py": 15975002719327784362, + "backend/src/services/clean_release/policy_resolution_service.py": 5075709738950902327, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/validation": 13992741783834278677, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_year.py": 17441853503700184299, + "venv/lib/python3.13/site-packages/pandas/io/formats/excel.py": 797619981868084011, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/fromnumeric.py": 3792079757208879362, + "frontend/build/_app/immutable/chunks/A2HVX04u.js": 9098839649209162783, + "backend/alembic/script.py.mako": 7041141123739810216, + "venv/lib/python3.13/site-packages/rsa/cli.py": 1753949198467694190, + "venv/lib/python3.13/site-packages/numpy/_typing/_shape.py": 12667553664122376205, + "venv/lib/python3.13/site-packages/pandas/_libs/sas.cpython-313-x86_64-linux-gnu.so": 16309838137741124637, + "venv/lib/python3.13/site-packages/pandas/core/groupby/categorical.py": 16588862198627920420, + "frontend/e2e/run-e2e.sh": 8106714116467101372, + "frontend/src/lib/stores/health.js": 1061942219531952418, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py": 9615571808720893016, + "venv/lib/python3.13/site-packages/pydantic/plugin/_schema_validator.py": 857640644840976862, + "venv/lib/python3.13/site-packages/anyio/abc/_streams.py": 8089057973257658396, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/interfaces.py": 5371646629319169153, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_unix.h": 9608771997528633681, + "venv/lib/python3.13/site-packages/jose/jwe.py": 3325660353225624025, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_unix.h": 13709837360414450184, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/validator.py": 14200019408839159476, + "venv/lib/python3.13/site-packages/iniconfig/_parse.py": 11418441008427032090, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/requirements.py": 11478648568700804705, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_complex.py": 12963449957363121481, + "venv/lib/python3.13/site-packages/pandas/io/pickle.py": 5418646169167774144, + "venv/lib/python3.13/site-packages/gitdb/test/test_base.py": 16305779366785148795, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_table.tpl": 8706980975714184465, + "backend/src/services/git_service.py": 1930048176535450912, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_color.py": 3700828402084646308, + "frontend/src/components/llm/__tests__/provider_config.integration.test.js": 12605115626277870995, + "frontend/test-results/translation.e2e.js-Transla-49baf-te-a-translation-job-via-UI-chromium/test-failed-1.png": 5297796663331551625, + "venv/lib/python3.13/site-packages/pip/_internal/operations/freeze.py": 3469845570614985559, + "frontend/src/lib/stores/activity.js": 14023344642294459631, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/RECORD": 16342326945648353387, + "frontend/src/lib/components/layout/__tests__/test_sidebar.svelte.js": 1424894894434906249, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_data.py": 2397937327057341, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/RECORD": 7320381591791379627, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh22648.pyf": 15094859289098837494, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/hybrid.py": 14287665090144592501, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils_md4.py": 15270721044910698449, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_formats.py": 10263085276270778330, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/processors.py": 4817637175145012571, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi": 7385072545198817771, + "venv/lib/python3.13/site-packages/keyring/testing/backend.py": 14295829193445781542, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/cli.py": 6382193953727339271, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nested_sequence.pyi": 10985533017688918283, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_searchsorted.py": 12074107109575676897, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.py": 17817123894903120316, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/RECORD": 1556051687830913019, + "venv/lib/python3.13/site-packages/uvicorn/py.typed": 15240312484046875203, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/array.py": 12658302948819867972, + "frontend/src/lib/components/dataset-review/SemanticLayerReview.svelte": 10805621354490209991, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-2.csv": 5423145402839576695, + "venv/lib/python3.13/site-packages/PIL/ContainerIO.py": 7551929914241379005, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_cross.py": 17742712348672218852, + "venv/lib/python3.13/site-packages/smmap/test/test_util.py": 5634593621291240094, + "venv/lib/python3.13/site-packages/pip/_internal/build_env.py": 3614358934066127900, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_bcrypt.py": 11146468670439284889, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_function.py": 7048185308307653546, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/macos.py": 17442393555211213147, + "venv/lib/python3.13/site-packages/numpy/f2py/__version__.py": 8588600299261398598, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/WHEEL": 7145482834744213753, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/RECORD": 12575893084587070195, + "venv/lib/python3.13/site-packages/httpcore/_trace.py": 9576253018891841832, + "venv/lib/python3.13/site-packages/pandas/util/_tester.py": 1033951890304257539, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_show_versions.py": 10577134135855180356, + "venv/lib/python3.13/site-packages/pytest_asyncio/plugin.py": 13414541723622354359, + "venv/lib/python3.13/site-packages/pandas/io/excel/_util.py": 6664989864423807444, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_list_accessor.py": 8613662386779664039, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/wsproto_impl.py": 3072714633272661847, + "venv/lib/python3.13/site-packages/pydantic/types.py": 785880062468926292, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.c": 9134176670140958459, + "venv/lib/python3.13/site-packages/click/termui.py": 7022374827798605437, + "venv/lib/python3.13/site-packages/pygments/lexers/objective.py": 10966124906622044512, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/RECORD": 12555714734722889695, + "venv/lib/python3.13/site-packages/pygments/lexers/arturo.py": 4565524340706235725, + "frontend/src/routes/settings/StorageSettings.svelte": 1953157077500445570, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args.py": 5821040955806257450, + "frontend/src/services/git-utils.js": 12982149055119314642, + "frontend/src/components/git/ConflictResolver.svelte": 2318922269446496715, + "backend/src/api/routes/dashboards/_projection.py": 1559143255780155174, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_py.py": 13809503317864539103, + "backend/src/api/routes/settings.py": 12171289292260529147, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/INSTALLER": 17282701611721059870, + "backend/src/models/clean_release.py": 6080208142954589647, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ma.pyi": 6467636381637286867, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/licenses/LICENSE": 6653202620765194772, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas.py": 9446226619493404132, + "venv/lib/python3.13/site-packages/numpy/typing/tests/test_typing.py": 10311989961588249147, + "venv/lib/python3.13/site-packages/_pytest/deprecated.py": 5452408732344157294, + "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.pyi": 16573082220135508510, + "venv/lib/python3.13/site-packages/packaging/_manylinux.py": 8268418900652790584, + "backend/src/__init__.py": 11250920420388310655, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_pickle.py": 2910383574326146698, + "venv/lib/python3.13/site-packages/anyio/_core/_streams.py": 6436461519284884601, + "venv/lib/python3.13/site-packages/numpy/random/_philox.pyi": 7503018432880404291, + "venv/lib/python3.13/site-packages/pandas/tests/computation/__init__.py": 15130871412783076140, + "backend/src/services/clean_release/repositories/compliance_repository.py": 7402628017251283691, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_localize.py": 4937713043686953098, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_monotonic.py": 4626065848596214108, + "venv/lib/python3.13/site-packages/pygments/lexers/parasail.py": 7758704230750989282, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/getitem.py": 556339939822508169, + "venv/lib/python3.13/site-packages/charset_normalizer/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/negative_bounds/issue_20853.f90": 3911644649277659333, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_formats.py": 2743326139283991615, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_periodindex.py": 13430003945574795792, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/WHEEL": 18203500019759199992, + "venv/lib/python3.13/site-packages/pydantic/_internal/_namespace_utils.py": 5237766366340181382, + "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.py": 2234931733687557433, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_timedelta.py": 17052461094717926538, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_riscv_unix.h": 8607317131295864391, + "venv/lib/python3.13/site-packages/pip/_internal/req/__init__.py": 12266077985828139928, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_regression.py": 12593643817685777170, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_deprecations.py": 14624516389510242394, + "venv/lib/python3.13/site-packages/numpy/_core/tests/_locales.py": 18178500267053825979, + "venv/lib/python3.13/site-packages/pluggy/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/io/_util.py": 13608922148814398500, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_upcast.py": 6496794053681247105, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/compiler.py": 11662494978292401568, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/_typing.py": 17598638528781632158, + "venv/lib/python3.13/site-packages/pygments/lexers/nit.py": 18262907536841158657, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufuncs.py": 12732599294816239111, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_missing.py": 2976310411820303374, + "frontend/src/lib/api.js": 1856641899861379694, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_indexing.py": 81180636019259296, + "frontend/build/_app/immutable/entry/app.CKeEP_5R.js": 7342639471182240759, + "backend/src/services/clean_release/mappers.py": 8684357784135236729, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict.py": 808180749933361743, + "venv/lib/python3.13/site-packages/pygments/lexers/tnt.py": 4657130609241407766, + "venv/lib/python3.13/site-packages/anyio/_core/_typedattr.py": 17584691045493791349, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_seed_sequence.py": 7687465232954593311, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/METADATA": 3044554568188160018, + "venv/lib/python3.13/site-packages/packaging/licenses/__init__.py": 15879493360260891293, + "venv/lib/python3.13/site-packages/pandas/_libs/parsers.cpython-313-x86_64-linux-gnu.so": 14134106903573638108, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/__init__.py": 12911374670789466961, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/_conditional.py": 14190849361275154667, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_droplevel.py": 2071197388394658690, + "venv/lib/python3.13/site-packages/packaging/licenses/_spdx.py": 16440031845441362300, + "frontend/build/_app/immutable/chunks/Dcsp6O9b.js": 9100273388100618859, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/licenses/LICENSE": 14524289631770661180, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/METADATA": 3188324716322823755, + "backend/src/api/routes/admin.py": 15812940668560248046, + "backend/src/plugins/translate/dictionary_validation.py": 16674754716565921261, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_interval_array_equal.py": 15654625365223535344, + "venv/lib/python3.13/site-packages/pygments/lexers/carbon.py": 6753624244809529841, + "backend/tests/services/clean_release/test_candidate_manifest_services.py": 16861589402854148894, + "backend/git_repos/remote/test-repo.git/hooks/fsmonitor-watchman.sample": 14381968406977928721, + "venv/lib/python3.13/site-packages/pip/_internal/cli/cmdoptions.py": 12107733000019393631, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_.py": 644667973700652263, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/METADATA": 4954909612926293404, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/base.py": 14908084548919567658, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/sync.py": 6189260178302531376, + "venv/lib/python3.13/site-packages/websockets/version.py": 5853118844880112885, + "backend/src/api/routes/assistant/_dataset_review.py": 14508672919199309579, + "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.py": 7658234162211356265, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.pyx": 5760108632332714287, + "venv/lib/python3.13/site-packages/pip/_internal/commands/configuration.py": 1608697090566485893, + "frontend/src/lib/components/assistant/__tests__/assistant_clarification.integration.test.js": 12258270009984504212, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_misc.py": 16393451200513733144, + "venv/lib/python3.13/site-packages/PIL/ImageColor.py": 17058618058554970738, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/util.py": 2176177435997659667, + "venv/lib/python3.13/site-packages/pygments/lexers/sgf.py": 6802546617450210439, + "frontend/src/routes/settings/git/+page.svelte": 9547035239167234477, + "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.pyi": 18193727720492050602, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_subclass.py": 16342917051908786497, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_range.py": 3544406211185340973, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.py": 14865906347579451546, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npz": 9481735723557962988, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_fixed.f90": 8079594129222364031, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/base.py": 5924592154431932923, + "venv/lib/python3.13/site-packages/passlib/tests/backports.py": 17098448557554464521, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_util.py": 16024147572807865219, + "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/mlib.ini": 16151597765994697599, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_interval.py": 14749388246573356959, + "backend/src/schemas/dataset_review.py": 15720223123457526104, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/METADATA": 16498441272591167134, + "backend/src/core/utils/superset_context_extractor/_parsing.py": 17093712504833649098, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/char.pyi": 1639810799219814643, + "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py": 1476468269208921287, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isna.py": 18394053826166267096, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/fields.py": 10090313990429695520, + "venv/lib/python3.13/site-packages/keyring/devpi_client.py": 3445890176686211982, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_equals.py": 11041884432990762650, + "venv/lib/python3.13/site-packages/charset_normalizer/cli/__init__.py": 9702005609966176016, + "backend/src/models/llm.py": 14295626272101666880, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/use_modules.f90": 3913612933340746297, + "venv/lib/python3.13/site-packages/pygments/lexers/sql.py": 15566262873151658034, + "venv/lib/python3.13/site-packages/pandas/core/arrays/period.py": 11560984428402318984, + "venv/lib/python3.13/site-packages/pandas/tests/internals/test_internals.py": 2049484047553296860, + "venv/lib/python3.13/site-packages/pip/_internal/models/installation_report.py": 14534544543677907640, + "venv/lib/python3.13/site-packages/pandas/_libs/join.cpython-313-x86_64-linux-gnu.so": 2947947484481830203, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_repr.py": 13147587049887075335, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply_relabeling.py": 3930092097678339743, + "venv/lib/python3.13/site-packages/pyasn1/type/tagmap.py": 14075859042659927673, + "venv/lib/python3.13/site-packages/itsdangerous/encoding.py": 205863946173486958, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/base_server.py": 9643197259080504542, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arithmetic.pyi": 10265823318697442495, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine.py": 4446011286411407708, + "venv/lib/python3.13/site-packages/dateutil/tz/__init__.py": 9617254832690762236, + "venv/lib/python3.13/site-packages/git/objects/commit.py": 12317865358846331802, + "venv/lib/python3.13/site-packages/pygments/filters/__init__.py": 2992007913496948339, + "frontend/src/lib/api/translate/jobs.js": 9613844538417291513, + "frontend/src/lib/ui/Select.svelte": 11354509509920497811, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/pkg_resources.py": 14271077832999370068, + "frontend/src/components/llm/ProviderConfig.svelte": 10540903648131362498, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_asfreq.py": 12721047184483392773, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_astype.py": 10171766562093740708, + "backend/src/services/git/_gitea.py": 16061594414833486958, + "backend/src/plugins/translate/scheduler.py": 7566547314463268121, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/test_support.pyi": 3893434234020223151, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_timezones.py": 4771674179164304731, + "backend/git_repos/remote/test-repo.git/hooks/pre-receive.sample": 6118492409684431911, + "venv/lib/python3.13/site-packages/git/types.py": 13960696584646279283, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi": 16312114056447366970, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/client_auth.py": 3301527187487914220, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi": 9405122752084210696, + "venv/lib/python3.13/site-packages/pandas/tests/internals/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/shell.py": 3842338968014057744, + "venv/lib/python3.13/site-packages/_pytest/pastebin.py": 13946273207624603063, + "venv/lib/python3.13/site-packages/pandas/_libs/reshape.cpython-313-x86_64-linux-gnu.so": 10639750501333255943, + "venv/lib/python3.13/site-packages/requests/hooks.py": 9888227370911151341, + "venv/lib/python3.13/site-packages/anyio/_core/_asyncio_selector_thread.py": 1760597191437520000, + "venv/lib/python3.13/site-packages/pyasn1/codec/native/__init__.py": 15728752901274520502, + "run.sh": 4808871544409245302, + "venv/lib/python3.13/site-packages/anyio/from_thread.py": 7973678341601453474, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2.py": 1331438198531942242, + "venv/lib/python3.13/site-packages/pygments/lexers/haxe.py": 1915270891525929751, + "backend/src/api/routes/dashboards/_action_routes.py": 11814954258886577640, + "venv/lib/python3.13/site-packages/numpy/char/__init__.pyi": 12214207343146244743, + "venv/lib/python3.13/site-packages/PIL/WalImageFile.py": 12442826195371137238, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/__init__.py": 15130871412783076140, + "frontend/build/favicon.png": 16740551781088792605, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_unicode.py": 1103608932321322989, + "backend/src/services/dataset_review/semantic_resolver.py": 8447496149691924829, + "venv/lib/python3.13/site-packages/psycopg2/extensions.py": 14197445323992851566, + "venv/lib/python3.13/site-packages/gitdb/test/__init__.py": 18393574323937353933, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_item.py": 17619314877770917080, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_features.py": 955759654189150497, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/applicator": 6870991911243684132, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING": 14459782230785388765, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/code.py": 9700811903057180696, + "frontend/src/components/DynamicForm.svelte": 12658559315809907844, + "venv/lib/python3.13/site-packages/passlib/ifc.py": 14479133654556657428, + "venv/lib/python3.13/site-packages/pandas/io/feather_format.py": 13242733585787888812, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/flatiter.pyi": 6885824496927786205, + "venv/lib/python3.13/site-packages/numpy/_configtool.py": 2946953194004101078, + "venv/lib/python3.13/site-packages/h11/_version.py": 9334144031400715479, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/api.py": 13134394673408254097, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_common.py": 10797243274520625867, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/__init__.py": 16434403703599680307, + "venv/bin/pyrsa-decrypt": 10929163076272054227, + "frontend/e2e/tests/login.e2e.js": 16303843677272482041, + "venv/lib/python3.13/site-packages/attr/__init__.pyi": 13203080513025719049, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/traceback.py": 2535227583726271746, + "frontend/playwright-report/index.html": 14077910029911296246, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py": 7111764828004587758, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/const_vs_enum.py": 12873459583943748931, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/jws.py": 7505219131537941130, + "frontend/playwright.config.js": 1601415616693761429, + "frontend/src/routes/settings/FeaturesSettings.svelte": 14068252633661039168, + "backend/src/core/superset_client/_datasets.py": 4813403545392307482, + "venv/lib/python3.13/site-packages/_pytest/unittest.py": 10848803633530338204, + "frontend/build/_app/immutable/chunks/BfNw3rzv.js": 1236526902294008250, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/markers.py": 8297667990263004097, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/WHEEL": 13347410390513723930, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connection.py": 8190661510572400096, + "venv/lib/python3.13/site-packages/numpy/_core/_methods.pyi": 9155045948490225716, + "venv/lib/python3.13/site-packages/pandas/_libs/ops.cpython-313-x86_64-linux-gnu.so": 10490009128553397250, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/accessor.py": 8373575175447947401, + "venv/lib/python3.13/site-packages/_pytest/tmpdir.py": 10834220332037364219, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_regression.py": 9375525053479844400, + "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimelike.py": 394614958716301173, + "venv/lib/python3.13/site-packages/pandas/tests/config/test_config.py": 4630144191929135452, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reset_index.py": 5946666595195952693, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_reductions.py": 14432782234851468060, + "frontend/src/lib/api/translate.js": 8957214461841000718, + "frontend/src/components/tasks/LogFilterBar.svelte": 5318774167984892372, + "backend/src/core/auth/security.py": 8289703096420999159, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py": 12775850858284584296, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/METADATA": 4868544280955034757, + "venv/lib/python3.13/site-packages/rpds/__init__.pyi": 12037474523494903587, + "venv/lib/python3.13/site-packages/numpy/core/_utils.py": 14108063453670701289, + "venv/lib/python3.13/site-packages/dateutil/__init__.py": 17666544837260663800, + "backend/src/services/git/_base.py": 9705469664041515959, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_crackfortran.py": 10278720705326601943, + "backend/src/api/routes/translate/_dictionary_routes.py": 5480137091791704976, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_join.py": 17508073052750406350, + "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py": 18317443814450756666, + "backend/src/services/git/_sync.py": 14233419521256437438, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/profiling.py": 16030397164203895281, + "frontend/src/lib/components/reports/__tests__/reports_list.ux.test.js": 16252194679528846126, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_hashtable.py": 2620207240930665390, + "backend/src/plugins/translate/orchestrator_query.py": 4760681990145471312, + "backend/tests/services/clean_release/test_approval_service.py": 8359796700245201345, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/RECORD": 10052481303799123207, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg.py": 8023549900603235601, + "frontend/src/lib/components/dataset-review/ValidationFindingsPanel.svelte": 2403276296409515448, + "backend/alembic/versions/2a7b8c9d0e1f_add_target_languages_to_translation_jobs.py": 7201017059554918882, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sinh.csv": 12358602501260667480, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_pbkdf2.py": 4977067068389993374, + "frontend/src/lib/stores/taskDrawer.js": 6659505033581293370, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/METADATA": 11557342107649194148, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py": 7906029092844029127, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq_py.py": 16054175754674285311, + "venv/lib/python3.13/site-packages/urllib3/fields.py": 9503202008440141421, + "backend/src/core/__tests__/test_config_manager_compat.py": 656412918792001580, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply.py": 12349190935180702377, + "venv/lib/python3.13/site-packages/PIL/PSDraw.py": 1927169421822446045, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_methods.py": 12694335778731418517, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/WHEEL": 691462085217888236, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/bitwise_ops.py": 14350609278188982939, + "backend/git_repos/remote/test-repo.git/objects/59/3a37acec7d2bddce10df90ab17bc9ede52e523": 1117161574576783985, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_parameter.py": 15173522343126004527, + "venv/lib/python3.13/site-packages/itsdangerous/timed.py": 12571102606719555007, + "venv/lib/python3.13/site-packages/requests/help.py": 1885413874621672882, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/aead.py": 8740476942925346444, + "venv/lib/python3.13/site-packages/pandas/api/extensions/__init__.py": 882327406374020760, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/key_set.py": 8485970749081951741, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict_of_blocks.py": 11646835537993064324, + "venv/lib/python3.13/site-packages/pandas/io/sas/__init__.py": 8095743504390517992, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/METADATA": 3179724236887811680, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py": 2609365889369107154, + "venv/lib/python3.13/site-packages/_pytest/pathlib.py": 9719734558816066453, + "venv/lib/python3.13/site-packages/psycopg2/sql.py": 18041240927130590808, + "venv/lib/python3.13/site-packages/pandas/tests/test_flags.py": 411845322592830527, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_take.py": 14969455660160053080, + "venv/lib/python3.13/site-packages/packaging/tags.py": 1999137344425005827, + "venv/lib/python3.13/site-packages/gitdb/utils/__init__.py": 15130871412783076140, + "backend/src/api/routes/dataset_review_pkg/_dependencies.py": 15939232213193276681, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_frame.py": 4303613319747945709, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_arithmetic.py": 17427754419626622853, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/watchfilesreload.py": 13062440477475586751, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test__version.py": 9037916251087760255, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_stmts.f90": 6123192245816905092, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_astype.py": 11995587502880659624, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_py.py": 6428078990080243608, + "venv/lib/python3.13/site-packages/pyasn1/compat/__init__.py": 5902203086150636670, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/setitem.py": 9552868454186447925, + "venv/lib/python3.13/site-packages/PIL/TgaImagePlugin.py": 9208942494595222259, + "venv/lib/python3.13/site-packages/pygments/lexers/scripting.py": 4167168074165473790, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/instrumentation.py": 13198832283145198959, + "venv/lib/python3.13/site-packages/pygments/lexers/jslt.py": 11805394130669644345, + "venv/lib/python3.13/site-packages/pygments/scanner.py": 5640381644804445932, + "backend/src/services/clean_release/dto.py": 11031410734734770552, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/licenses/LICENSE": 17020744174398828059, + "venv/lib/python3.13/site-packages/numpy/_core/memmap.pyi": 14124415711399771715, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/meson.build": 8784756284679859367, + "venv/lib/python3.13/site-packages/yaml/_yaml.cpython-313-x86_64-linux-gnu.so": 16990140871649227895, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/__init__.py": 6758781485387665020, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/engines.py": 10876954465293959720, + "venv/lib/python3.13/site-packages/PIL/ImageShow.py": 11437096951764124817, + "venv/lib/python3.13/site-packages/PIL/FpxImagePlugin.py": 18245056799205041866, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py": 15077617224435782028, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/exc.py": 1448374203226983757, + "venv/lib/python3.13/site-packages/cryptography/__init__.py": 641702726372647770, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.py": 5997687240967250164, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/client_credentials.py": 256154392957334831, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/METADATA": 6960169108624303030, + "backend/src/api/routes/git/_router.py": 8701683910668868827, + "backend/src/plugins/translate/dictionary.py": 2903767700173238799, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/sdist.py": 9490366272695301385, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/encoder.py": 8912119133548489544, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.asm": 16630507336149184509, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/distro.py": 12494639376332486204, + "venv/lib/python3.13/site-packages/websockets/server.py": 16599051743322571790, + "venv/lib/python3.13/site-packages/pip/_internal/operations/install/__init__.py": 10456333337205488584, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ec.pyi": 9835891299056543360, + "venv/lib/python3.13/site-packages/pydantic/v1/config.py": 12050640083719340958, + "venv/lib/python3.13/site-packages/numpy/typing/__init__.py": 3608665862759745048, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval_pyarrow.py": 3849382723890414899, + "backend/src/services/clean_release/__init__.py": 18311663871116449547, + "backend/ss_tools_backend.egg-info/PKG-INFO": 15134841326777953914, + "venv/lib/python3.13/site-packages/websockets/asyncio/server.py": 885415484558949506, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/revocation.py": 16178499278312777474, + "venv/lib/python3.13/site-packages/pandas/_libs/lib.cpython-313-x86_64-linux-gnu.so": 6452315003728497116, + "frontend/src/lib/utils.js": 5436516952434475239, + "backend/src/api/routes/assistant/_llm_planner.py": 444517242115816139, + "venv/lib/python3.13/site-packages/PIL/BufrStubImagePlugin.py": 5520941821175895889, + "venv/lib/python3.13/site-packages/pandas/core/interchange/buffer.py": 12961802206383700922, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/gh19161.f90": 7691871466158049079, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray.pyi": 14369009813591437391, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py": 11808051681178127650, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/base.py": 10200916670889920659, + "venv/lib/python3.13/site-packages/fastapi/middleware/__init__.py": 5289703478893407762, + "venv/lib/python3.13/site-packages/annotated_types/test_cases.py": 5123395674810472016, + "venv/lib/python3.13/site-packages/pydantic/main.py": 14086938579850319884, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_pyf_src.py": 12351476647369971852, + "venv/lib/python3.13/site-packages/passlib/handlers/misc.py": 18255658377109333797, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/WHEEL": 18203500019759199992, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_string.py": 15542851215119662814, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_formats.py": 17821648708413143557, + "venv/lib/python3.13/site-packages/pycparser/c_lexer.py": 10964496587169353564, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_index_tricks.py": 4676293127859087991, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_old_base.py": 191359377446257900, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/base.py": 11386927749822539057, + "venv/bin/pygmentize": 2166491742035772877, + "venv/lib/python3.13/site-packages/pydantic/_internal/_repr.py": 1758478941817868344, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/twodim_base.pyi": 3773329770170248877, + "venv/lib/python3.13/site-packages/smmap/mman.py": 9091562788592406032, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/logging.py": 2426447364230673649, + "venv/lib/python3.13/site-packages/jose/__init__.py": 15575670403380555945, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/__init__.py": 18069619555394133945, + "venv/lib/python3.13/site-packages/pyasn1/codec/native/encoder.py": 120921270972591880, + "venv/lib/python3.13/site-packages/greenlet/TExceptionState.cpp": 17617354576371846109, + "venv/lib/python3.13/site-packages/pydantic/v1/validators.py": 1060954771151908883, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_alpha_unix.h": 2759424570606117942, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo90.f90": 15981076495193915107, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/_typing.py": 17795648994662434211, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/RECORD": 6349071557579241461, + "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.pyi": 5892973170789971535, + "backend/src/services/superset_lookup_service.py": 16520439978260397080, + "venv/lib/python3.13/site-packages/anyio/abc/_testing.py": 16484863782452886547, + "backend/src/models/dataset_review.py": 5814500849969423218, + "backend/git_repos/remote/test-repo.git/refs/heads/feature/e2e-update": 9577764946931187657, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_indexing.py": 6039808271042864401, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_palettes.py": 13495896173068490516, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_round.py": 9358309072340573675, + "venv/lib/python3.13/site-packages/greenlet/tests/test_gc.py": 6383305344377882524, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo90.f90": 58572216018544419, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_get_dummies.py": 14871264482622327003, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_unsupported.py": 9115440166074553860, + "backend/git_repos/remote/test-repo.git/refs/heads/main": 7667767390913789840, + "venv/lib/python3.13/site-packages/numpy/strings/__init__.pyi": 6213864891045042045, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/rsa.py": 5392922995791487437, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/issue232.py": 6498198296113395367, + "venv/lib/python3.13/site-packages/dotenv/variables.py": 5438726379749803751, + "venv/lib/python3.13/site-packages/pydantic/functional_validators.py": 5835339743072048940, + "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending_distributions.py": 209769687436320665, + "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.py": 1731362089136926666, + "venv/lib/python3.13/site-packages/pygments/styles/tango.py": 12389250878249075539, + "frontend/src/routes/datasets/review/useReviewSession.js": 2378011913105385059, + "venv/lib/python3.13/site-packages/pygments/lexers/rego.py": 16022844238903541738, + "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.pyi": 15509415708991012734, + "venv/lib/python3.13/site-packages/numpy/_core/strings.pyi": 1981676808027146409, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_numeric.py": 9417128508433693658, + "venv/lib/python3.13/site-packages/PIL/JpegPresets.py": 12342262642420923471, + "venv/lib/python3.13/site-packages/keyring/backends/kwallet.py": 3071915635647280308, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-1.csv": 5708101386018266998, + "backend/src/services/profile_preference_service.py": 58722622434127499, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_core_functionalities.py": 10449747865888948579, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_expanding.py": 12504275097476819918, + "venv/lib/python3.13/site-packages/ecdsa/test_numbertheory.py": 2117130219401053225, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tanh.csv": 6674536825156196036, + "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.py": 4154852745131464543, + "venv/lib/python3.13/site-packages/greenlet/greenlet_exceptions.hpp": 8819301638282185623, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/provision.py": 17812733620485633975, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape.pyi": 17197248408326188931, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py": 16571782049241458769, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_character.py": 850554362222586199, + "backend/src/plugins/translate/_batch_insert.py": 12482635271028646257, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/naming.py": 14503932124671438340, + "frontend/src/routes/dashboards/[id]/+page.svelte": 12833231703682809118, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/criterion.py": 2042509334195547606, + "frontend/src/lib/ui/Card.svelte": 3442526978752993023, + "venv/lib/python3.13/site-packages/anyio/_core/_eventloop.py": 6853955946028131785, + "venv/lib/python3.13/site-packages/numpy/random/_pcg64.pyi": 14785496840737986355, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi": 1065808244870331598, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_astype.py": 7542068949396266883, + "venv/lib/python3.13/site-packages/pip/_internal/locations/_sysconfig.py": 4773855595767526182, + "venv/lib/python3.13/site-packages/httpx/_models.py": 6516278895118988231, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_records.py": 6252761812228230408, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_abstract_interface.py": 17625870734108222276, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE": 17284252174641192866, + "venv/lib/python3.13/site-packages/secretstorage/exceptions.py": 16701299261122689924, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/models.py": 17870758731501836303, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/matrix.pyi": 15667024840812783911, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/methods.py": 8059372834301557565, + "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/test_decimal.py": 17811784255174057561, + "venv/lib/python3.13/site-packages/PIL/ImageChops.py": 16368812693381154921, + "venv/lib/python3.13/site-packages/PIL/MicImagePlugin.py": 13540523615822507963, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/scripts.py": 8759605303886441976, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_libsparse.py": 1859997684694949029, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/_libs/writers.pyi": 10494773026133244183, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpq-9b38f5e3.so.5.17": 9680474575710585826, + "frontend/src/lib/stores/__tests__/mocks/state.js": 3616783395254238777, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py": 15735839022111230173, + "frontend/build/_app/immutable/chunks/DSaq_U9T.js": 18007300971386663499, + "backend/src/core/auth/__tests__/test_auth.py": 14999763239648705195, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/shape.py": 13410030168622484801, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/_cffi_backend.cpython-313-x86_64-linux-gnu.so": 829323374625147265, + "frontend/vite.config.js": 982595057268821071, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numeric.py": 5627177940393829333, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tolist.py": 8343058000369672317, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_errstate.py": 10788459171326750780, + "venv/lib/python3.13/site-packages/_pytest/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/anyio/_core/__init__.py": 15130871412783076140, + "backend/src/plugins/translate/dictionary_crud.py": 17858252266960667195, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/__init__.py": 14522970260621605208, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_astype.py": 4178231007649608531, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_isocalendar.py": 5704842983090753914, + "backend/src/services/dataset_review/clarification_pkg/_helpers.py": 4822867945130807759, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.py": 2401931196871484449, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_setops.py": 5208912445718807372, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth1_client.py": 3129955464287331242, + "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/__init__.py": 9083396324667433335, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo77.f": 16156304612888138777, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.py": 4111797647365870137, + "venv/lib/python3.13/site-packages/idna/core.py": 7725813731640753267, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/WHEEL": 14929202952940710322, + "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.py": 1644743742545329282, + "venv/lib/python3.13/site-packages/PIL/_webp.pyi": 18222325750818585549, + "venv/lib/python3.13/site-packages/PIL/Jpeg2KImagePlugin.py": 16905402423248975204, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_mangle_dupes.py": 15613858733226279352, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/assertion_client.py": 4261295109880943573, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py": 15377043158761455858, + "venv/lib/python3.13/site-packages/greenlet/greenlet.h": 16216120538647882082, + "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.ux.test.js": 5439236328736424365, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_store.py": 1500300322083708794, + "frontend/src/routes/login/+page.svelte": 18430869485200259448, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_interpolate.py": 6154072863387383113, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/pyinstaller-smoke.py": 13570990531070630950, + "venv/lib/python3.13/site-packages/charset_normalizer/md__mypyc.cpython-313-x86_64-linux-gnu.so": 12629680116730190225, + "venv/lib/python3.13/site-packages/pip/_internal/models/format_control.py": 12916176880659590456, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust.abi3.so": 2537152913308578487, + "venv/lib/python3.13/site-packages/numpy/_core/_simd.pyi": 13553578040186939249, + "venv/lib/python3.13/site-packages/passlib/handlers/sha2_crypt.py": 8171587560325812664, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_map.py": 6556132946198785761, + "backend/git_repos/remote/test-repo.git/objects/f7/6ee984d9d47c3cd0b362fdf60a47162bbc202d": 5351983130474596028, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py": 14954780419054588164, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-2.csv": 14641856424618845798, + "venv/lib/python3.13/site-packages/referencing/tests/test_referencing_suite.py": 8875120876162738440, + "venv/lib/python3.13/site-packages/pygments/lexers/matlab.py": 1880915927722177654, + "venv/lib/python3.13/site-packages/pydantic/version.py": 3699343596421001359, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_pad.pyi": 14755435694146003896, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py": 3515468590005709183, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/dml.py": 5340326603935367534, + "venv/lib/python3.13/site-packages/pygments/lexers/lisp.py": 11872331622183590758, + "venv/lib/python3.13/site-packages/authlib/oauth1/__init__.py": 3692257288065292607, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/toml.py": 2225382735669153136, + "venv/lib/python3.13/site-packages/pygments/lexers/textfmts.py": 17438453283146671997, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/fastapi/openapi/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/util.py": 4890614159013873000, + "venv/lib/python3.13/site-packages/pip/_internal/main.py": 13711402795965346761, + "venv/lib/python3.13/site-packages/pandas/_libs/parsers.pyi": 18402185996588014111, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_na_values.py": 13527837495439739279, + "frontend/build/_app/immutable/nodes/21.D6n4_eiK.js": 1193033676731993975, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/abstract.py": 14216912708889351810, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/base.py": 5294446834530589760, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/random/_common.pxd": 3402284125943848851, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh26681.f90": 1515497186927052106, + "venv/lib/python3.13/site-packages/py.py": 16248211404003181569, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_argsort.py": 7320661835159648143, + "venv/lib/python3.13/site-packages/sqlalchemy/util/deprecations.py": 7020540470348910770, + "frontend/src/lib/ui/LanguageSwitcher.svelte": 3899734183369041614, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libssl-81ffa89e.so.3": 13913673119166658781, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_ctors.py": 631744056514965940, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/INSTALLER": 17282701611721059870, + "backend/src/plugins/translate/__tests__/test_dictionary_prompt_builder.py": 12854863376472707556, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/validator_creation.py": 2745563768861102999, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_version.pyi": 18093228021414910594, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_shift.py": 14681514853406253657, + "venv/lib/python3.13/site-packages/pygments/lexers/ncl.py": 14838368780033177943, + "frontend/src/components/TaskRunner.svelte": 18014857434320509117, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/protocol.py": 932296179250981622, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/lower_f2py_fortran.f90": 13315882216322269383, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/websockets/http11.py": 11022534176398053082, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/sqlalchemy/event/api.py": 987321343916500337, + "venv/lib/python3.13/site-packages/pycparser/ast_transforms.py": 9032453625977464506, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/parse.py": 2427019397934437210, + "venv/lib/python3.13/site-packages/authlib/common/errors.py": 16082974228565515177, + "venv/lib/python3.13/site-packages/pygments/lexers/dns.py": 17149871769553794654, + "backend/src/core/superset_client/_databases.py": 14837101840847724937, + "backend/src/services/dataset_review/orchestrator.py": 8780497744077644822, + "venv/lib/python3.13/site-packages/dotenv/__main__.py": 16883579879172186355, + "venv/lib/python3.13/site-packages/pydantic/_internal/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.pyi": 3814281717779245045, + "venv/lib/python3.13/site-packages/fastapi/concurrency.py": 17937688838625807004, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_histograms.py": 47631283818191049, + "venv/lib/python3.13/site-packages/PIL/_imagingft.pyi": 1385456134120145584, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd.py": 7577408467041941595, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_collections.py": 14307204206312829238, + "frontend/src/lib/stores/translationRun.js": 10005138082029605368, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/WHEEL": 12054782055241301457, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_regression.py": 16266600037241221681, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/testing.pyi": 16263777835572515739, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_records.py": 18167578114159312327, + "backend/src/plugins/translate/metrics.py": 16289306156708185830, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/RECORD": 7266315886702289726, + "backend/src/api/routes/storage.py": 17448492494022569347, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_numeric.py": 9383693732009547935, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_csv.py": 571991535656576735, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_halfyear.py": 9376272342364384767, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/scrypt.py": 15294662527620994511, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_mixed.py": 3476101699679108025, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_formats.py": 9738552308554950419, + "venv/lib/python3.13/site-packages/pandas/core/indexes/base.py": 496956059910401121, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__init__.py": 8880435562116660648, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/__init__.py": 15130871412783076140, + "backend/alembic/versions/ed310b33f02c_multi_language_translation_tables.py": 1424919631205944664, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_compat.py": 6032546839876195115, + "venv/lib/python3.13/site-packages/pip/_internal/cache.py": 210214424535803946, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dsa.py": 8079219938116542284, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/RECORD": 15194367076254806710, + "venv/lib/python3.13/site-packages/pandas/core/indexing.py": 15507869313676722071, + "venv/lib/python3.13/site-packages/urllib3/response.py": 1987354357306756790, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/errors.py": 10750605030539582738, + "venv/lib/python3.13/site-packages/pygments/lexers/rebol.py": 10460732731157940232, + "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.pyi": 18290764713161282980, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timezones.py": 8042029178538467146, + "backend/src/plugins/translate/__tests__/test_dictionary_utils.py": 12007616543185225081, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/evaluator.py": 6348022789899831255, + "venv/lib/python3.13/site-packages/requests/models.py": 9104898985870652256, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_masked.py": 5380551301916549925, + "venv/lib/python3.13/site-packages/pandas/tests/frame/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/METADATA": 10707390829735099042, + "venv/lib/python3.13/site-packages/more_itertools/recipes.pyi": 16788810745659802820, + "venv/lib/python3.13/site-packages/starlette/exceptions.py": 15978811612997200737, + "venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py": 9639351759822608724, + "venv/lib/python3.13/site-packages/pip/_internal/commands/inspect.py": 1401259241736437656, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/index_tricks.pyi": 6035507586990711471, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/__init__.py": 4390908730288159455, + "frontend/src/routes/translate/[id]/+page.svelte": 5708982801369658186, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/METADATA": 6502714564886871819, + "venv/lib/python3.13/site-packages/pandas/tests/extension/conftest.py": 16886862391161243448, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/common.py": 16314033415206173235, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/jeepney/tests/test_bindgen.py": 8637878018232418583, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/RECORD": 6187512110158566509, + "venv/lib/python3.13/site-packages/authlib/oidc/discovery/well_known.py": 8259335680633926377, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_query_eval.py": 17613800806748649144, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_s3.py": 17425535047477778484, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_sparc_sun_gcc.h": 2915128971664712180, + "venv/lib/python3.13/site-packages/keyring/backends/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/anyio/streams/memory.py": 11762722143503428823, + "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.py": 338595621218646492, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_join.py": 17874591841487372992, + "venv/lib/python3.13/site-packages/pandas/tests/test_downstream.py": 5635510753354256507, + "venv/lib/python3.13/site-packages/passlib/handlers/sha1_crypt.py": 5752556581809938954, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_scalar_compat.py": 10068989979144965125, + "venv/lib/python3.13/site-packages/numpy/random/_sfc64.cpython-313-x86_64-linux-gnu.so": 17186695607612591922, + "venv/lib/python3.13/site-packages/authlib/oidc/registration/claims.py": 4161520263207458028, + "venv/lib/python3.13/site-packages/pydantic/_internal/_typing_extra.py": 14843908493613991959, + "venv/lib/python3.13/site-packages/pandas/io/sas/sas7bdat.py": 17851617851457266799, + "venv/lib/python3.13/site-packages/PIL/ImageQt.py": 6607270889359138659, + "venv/lib/python3.13/site-packages/fastapi/exception_handlers.py": 9907950997607808699, + "venv/lib/python3.13/site-packages/greenlet/_greenlet.cpython-313-x86_64-linux-gnu.so": 6036818030377883416, "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_searchsorted.py": 11161416745923343741, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.pyi": 6616985736007157309, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/hkdf.py": 8539232219382152238, - "venv/lib/python3.13/site-packages/sqlalchemy/event/base.py": 18238453132776154057, - "venv/lib/python3.13/site-packages/ecdsa/keys.py": 10281170640484982154, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py": 7732330576029516854, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/errors.py": 14634768861119075506, + "venv/lib/python3.13/site-packages/git/remote.py": 1694919255653049944, + "venv/lib/python3.13/site-packages/pygments/styles/perldoc.py": 4857683935715743356, + "venv/bin/pyrsa-keygen": 8222542582170363672, + "venv/lib/python3.13/site-packages/pyasn1/codec/der/decoder.py": 11876648884087997745, + "frontend/src/routes/storage/repos/+page.svelte": 3849368960648953479, + "backend/src/schemas/settings.py": 7182606947764184429, + "backend/src/services/profile_service.py": 7430451608615934464, + "backend/src/plugins/translate/__init__.py": 12400577052797809611, + "venv/lib/python3.13/site-packages/numpy/_core/strings.py": 3623885046208831106, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_utils.py": 11574890848475750509, + "venv/lib/python3.13/site-packages/pandas/tests/base/common.py": 13335865314907577354, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_clip.py": 11111183698178681331, + "venv/lib/python3.13/site-packages/authlib/common/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/doc/ufuncs.py": 11706048944901534664, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_ndarray.py": 6863874753941288585, + "venv/lib/python3.13/site-packages/pandas/__init__.py": 793698514004377486, + "venv/lib/python3.13/site-packages/urllib3/util/response.py": 6106698559175827207, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/claims.py": 11853156183819186599, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/__init__.py": 8558541715982163200, + "venv/lib/python3.13/site-packages/attrs/__init__.pyi": 14353885498883901855, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py": 71159121705871510, + "venv/lib/python3.13/site-packages/urllib3/contrib/pyopenssl.py": 11839715589709210412, + "venv/lib/python3.13/site-packages/pygments/lexers/macaulay2.py": 8022891778577314212, + "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.pyi": 14181518821845356636, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numeric.pyi": 17005748821769179511, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_to_xml.py": 8477770807916990981, + "venv/lib/python3.13/site-packages/psycopg2/extras.py": 7510107741729866600, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_equals.py": 9405821443172167066, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_repeat.py": 11360772447747837958, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_string.py": 10141066226714805009, + "venv/lib/python3.13/site-packages/pandas/core/api.py": 3390809109543982410, + "venv/lib/python3.13/site-packages/httpx/_decoders.py": 14478643473006178492, + "venv/lib/python3.13/site-packages/pyasn1/codec/cer/encoder.py": 3261739474564948330, + "venv/lib/python3.13/site-packages/jeepney/io/blocking.py": 4699555965372200059, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_numeric.py": 17391095502314929042, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctanh.csv": 8620268345075425735, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npy": 17303060773267383537, + "venv/lib/python3.13/site-packages/pygments/lexers/hare.py": 8693002231020751711, + "frontend/src/components/MissingMappingModal.svelte": 11264881226066623003, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numeric_only.py": 13610631681447961840, + "venv/lib/python3.13/site-packages/numpy/_distributor_init.py": 1101690714183863290, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/websockets/sync/client.py": 10892557975919033983, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ma.py": 15912582628112164509, + "venv/lib/python3.13/site-packages/pip/_internal/network/utils.py": 13421242949994555055, + "venv/lib/python3.13/site-packages/urllib3/connectionpool.py": 7493427857878469165, + "venv/lib/python3.13/site-packages/gitdb/utils/encoding.py": 1668793658272579497, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test__exceptions.py": 12099001631178696189, + "backend/src/core/superset_client/_datasets_preview_filters.py": 12835417730902013107, + "venv/lib/python3.13/site-packages/pygments/styles/dracula.py": 12379322008538174078, + "venv/lib/python3.13/site-packages/itsdangerous/__init__.py": 6969210114978365656, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64_np126.pkl.gz": 1630203486813146863, + "frontend/src/components/git/useGitManager.js": 5317184945628692154, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/__init__.py": 15130871412783076140, + "backend/alembic/versions/8dd0a93af539_drop_deprecated_source_language_column_.py": 3574830605384611294, + "venv/lib/python3.13/site-packages/click/_utils.py": 16491408631876616271, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/RECORD": 13494296690334916257, + "frontend/build/_app/immutable/chunks/Bu6g0tul.js": 17283269274186886249, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_reflection.py": 10520778598547003532, + "backend/src/plugins/translate/_llm_call.py": 698303364119968671, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_common.py": 12064185052519470358, + "backend/src/api/routes/dashboards/_helpers.py": 16683849709430404975, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_types.py": 651959450717569029, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/memmap.pyi": 15629663272474054909, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/licenses/LICENSE": 1641332741515411734, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh27697.f90": 4990763135487995517, + "venv/lib/python3.13/site-packages/fastapi/_compat/v2.py": 15078491685917047095, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_argparse.py": 11111699418217357084, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/collections.py": 16114931075157411913, + "venv/lib/python3.13/site-packages/pandas/io/formats/csvs.py": 17223598248259691292, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/fastapi/openapi/docs.py": 16735218221639179927, + "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.py": 7029935879540617057, + "venv/lib/python3.13/site-packages/itsdangerous/serializer.py": 14271229091461164933, + "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.pyi": 7171810177801103765, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayterator.pyi": 7439348655567232744, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/passlib/handlers/scram.py": 12281469744083867450, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/mock.py": 14010776046610111182, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpcre-9513aab5.so.1.2.0": 12298861477332583668, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/METADATA": 14036738037171597049, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.pyi": 15498895254516573071, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.pyi": 11967691665766452312, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta.py": 7742506012012398830, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/strategies.py": 14007486769065345085, + "venv/lib/python3.13/site-packages/websockets/asyncio/compatibility.py": 9716993942839407921, + "venv/lib/python3.13/site-packages/_pytest/tracemalloc.py": 5530234062530858720, + "venv/lib/python3.13/site-packages/fastapi/middleware/cors.py": 7452860773712601508, + "venv/lib/python3.13/site-packages/cffi/vengine_cpy.py": 10684192171370688079, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_ratio.py": 940213488194452623, "venv/lib/python3.13/site-packages/cffi/setuptools_ext.py": 30796527302561458, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_fileno.py": 15541504502174047167, - "venv/lib/python3.13/site-packages/pydantic_settings/version.py": 17263591404988984480, - "venv/lib/python3.13/site-packages/websockets/legacy/client.py": 4032878587330592478, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_utils.pyi": 12312677132342982866, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/token.py": 4161760290367932793, - "venv/lib/python3.13/site-packages/pandas/core/sample.py": 11913688303879867963, - "venv/lib/python3.13/site-packages/pandas/core/indexes/multi.py": 9115751979327393200, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_formats.py": 5259100336852479395, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/RECORD": 7810411361439269754, - "venv/lib/python3.13/site-packages/urllib3/util/__init__.py": 2848870790459558599, + "venv/lib/python3.13/site-packages/numpy/f2py/__init__.py": 7470157159992827389, + "venv/lib/python3.13/site-packages/jose/jwt.py": 3405491994268385178, + "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.py": 14446508217220482719, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_join.py": 1131792466825856990, + "venv/lib/python3.13/site-packages/pygments/lexers/jvm.py": 897669925284011089, + "venv/lib/python3.13/site-packages/pygments/lexers/rnc.py": 9747352274729854620, + "venv/lib/python3.13/site-packages/pygments/lexers/javascript.py": 16869490851285457857, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/codec.py": 14814999235499530522, + "venv/lib/python3.13/site-packages/pandas/core/col.py": 4221271461723119525, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/multiprocess.py": 10047012560534876149, + "venv/lib/python3.13/site-packages/pygments/lexers/smalltalk.py": 6113431347120223592, + "backend/src/core/utils/__init__.py": 12454773556575864508, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/RECORD": 9678861487447993169, + "venv/lib/python3.13/site-packages/pygments/lexers/sophia.py": 15971500155124949058, + "venv/lib/python3.13/site-packages/pygments/lexers/console.py": 18266795648198475028, + "backend/src/core/__tests__/test_superset_preview_pipeline.py": 5338719285630543394, + "frontend/src/lib/ui/Button.svelte": 921807193049478213, + "venv/lib/python3.13/site-packages/jsonschema/tests/_suite.py": 6307666366417438489, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_align.py": 6050248183327934780, + "venv/lib/python3.13/site-packages/pygments/lexers/_mapping.py": 12987629396101138833, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/endpoint.py": 13389763148720370546, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/modules.pyi": 14176187662591164602, + "venv/lib/python3.13/site-packages/packaging/_structures.py": 13086687542872305890, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_values.py": 430620320423334461, + "frontend/src/lib/components/reports/__tests__/report_detail.integration.test.js": 4193615240297330385, + "backend/src/plugins/translate/__tests__/__init__.py": 2490542523242946173, + "venv/lib/python3.13/site-packages/keyring/http.py": 1408024724330078009, + "venv/lib/python3.13/site-packages/pandas/conftest.py": 747626550806435763, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/clsregistry.py": 1589692649837935355, + "backend/alembic/versions/543d43d752b8_migrate_old_dictionary_entries_and_.py": 10988663686444687907, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/__init__.py": 4770885427721822507, + "venv/lib/python3.13/site-packages/numpy/core/function_base.py": 12696640832638144604, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/roles.py": 5448455900674232810, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.cpython-313-x86_64-linux-gnu.so": 657138415665704488, + "venv/lib/python3.13/site-packages/pandas/tests/api/test_api.py": 13385690673368400635, + "venv/lib/python3.13/site-packages/pygments/lexers/vip.py": 11287963054551122004, + "frontend/build/_app/immutable/assets/AssistantChatPanel.D4L5jlt0.css": 10724144138366384732, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/warnings_and_errors.pyi": 3151453262733619734, + "venv/lib/python3.13/site-packages/pygments/lexers/bibtex.py": 14652372822443154388, + "backend/src/scripts/seed_permissions.py": 2712668643177555956, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/extension/json/array.py": 7437107031079833232, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/resource_protector.py": 17080006010679257864, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_values.py": 12064896795288302730, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_common.py": 9097104320321411408, + "venv/lib/python3.13/site-packages/pygments/lexers/spice.py": 1376126320935115350, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/WHEEL": 7414522633964952515, + "backend/src/api/routes/dataset_review.py": 3689614039699023362, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_where.py": 3272402211717453881, + "venv/lib/python3.13/site-packages/pycparser/ply/ctokens.py": 1599952198962458730, + "venv/lib/python3.13/site-packages/httpx/_transports/mock.py": 13564349939188292363, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.cpython-313-x86_64-linux-gnu.so": 15960606468596245469, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/functions.py": 14107377669163193871, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/style.py": 15214295655795348821, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/manifest.py": 16998683210177680416, + "venv/lib/python3.13/site-packages/pip/_internal/commands/lock.py": 14782882714168174465, + "venv/lib/python3.13/site-packages/pygments/modeline.py": 6194365452131561478, + "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.py": 2215312866861772848, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_time_grouper.py": 3203729024111501071, + "backend/src/models/auth.py": 8146256910993395749, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_envs.py": 5657151394155576509, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_kind.py": 18178830861475480607, + "frontend/src/lib/components/translate/BulkCorrectionSidebar.svelte": 10114621552038076221, + "frontend/src/lib/components/translate/TranslationRunResult.svelte": 3992997916593732645, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel_py.py": 9112851333217812996, + "venv/lib/python3.13/site-packages/_pytest/terminal.py": 885450969703476729, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_partial.py": 3151286293440416036, + "venv/lib/python3.13/site-packages/pandas/io/sas/sas_constants.py": 14492427939536692313, + "venv/lib/python3.13/site-packages/PIL/DcxImagePlugin.py": 11767274218348117481, + "frontend/src/lib/stores/__tests__/test_datasetReviewSession.js": 12620073136419991103, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot.py": 334735766122314163, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/__init__.py": 15728752901274520502, + "backend/src/plugins/llm_analysis/__init__.py": 9254812131558249924, + "venv/lib/python3.13/site-packages/referencing/_core.py": 14744795336121513230, + "venv/lib/python3.13/site-packages/anyio/functools.py": 4300781896135050327, + "venv/lib/python3.13/site-packages/starlette/middleware/base.py": 16850428285382256618, + "venv/lib/python3.13/site-packages/pandas/tests/strings/__init__.py": 5060190599837877599, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_python_parser_only.py": 12231449578582108346, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/authorization_server.py": 2491654903543600678, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/utils.py": 8633356372647820801, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/engine.py": 2652248727360250985, + "venv/lib/python3.13/site-packages/PIL/_imagingcms.pyi": 12476347428820346383, + "venv/lib/python3.13/site-packages/websockets/extensions/permessage_deflate.py": 12957105912018691390, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/attrs/filters.py": 6844006230924162452, + "venv/lib/python3.13/site-packages/cryptography/x509/extensions.py": 10197248868682803019, + "venv/lib/python3.13/site-packages/rpds/__init__.py": 5862197328247012275, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/WHEEL": 3749476255729626245, + "venv/lib/python3.13/site-packages/numpy/ma/mrecords.py": 687170905107114309, + "venv/lib/python3.13/site-packages/numpy/linalg/__init__.pyi": 18409133021422984774, + "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.py": 15558318749939680074, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.pyi": 10360538024015064036, + "venv/lib/python3.13/site-packages/referencing/py.typed": 15130871412783076140, + "frontend/e2e/tests/migration.e2e.js": 12564023806718668322, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_find_replace.py": 262184394614748386, + "dist/docker/ss-tools.20260517.tar.xz": 10542870312371880474, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_constructors.py": 1697610790604400105, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/validation": 5398883285871046811, + "venv/lib/python3.13/site-packages/gitdb/db/git.py": 13552203494737962026, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connectionpool.py": 10453724680327835821, + "frontend/src/components/Footer.svelte": 17748559656923032, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/requests.py": 14847842002253212098, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunclike.py": 12077553320554633586, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py": 9224234539232195189, + "venv/lib/python3.13/site-packages/pygments/lexers/_mysql_builtins.py": 16087227298679432006, + "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/test_rapidfuzz_packaging.py": 5189055608193366093, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token_validator.py": 2932866595454490527, + "backend/src/core/mapping_service.py": 15428037653286485748, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/typing_extensions.py": 8402965285733247527, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/auth.py": 13542332344403032059, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_ufunc.py": 3326502989311831856, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_time_series.py": 5205199092955498507, + "backend/src/api/routes/__tests__/test_git_status_route.py": 11848061697215496182, + "backend/tests/core/test_git_service_gitea_pr.py": 6098719224379185148, + "venv/lib/python3.13/site-packages/pip/_internal/index/package_finder.py": 12631116673181479779, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/resource_protector.py": 14354891358060085360, + "venv/lib/python3.13/site-packages/anyio/to_process.py": 16627578499791368365, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/WHEEL": 9100692507274722091, + "venv/lib/python3.13/site-packages/numpy/_core/multiarray.pyi": 3494871767602648807, + "venv/lib/python3.13/site-packages/pandas/_libs/missing.cpython-313-x86_64-linux-gnu.so": 1990755879067132155, + "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django.py": 3210697476292094384, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py": 16951268065271931235, + "venv/lib/python3.13/site-packages/fastapi/logger.py": 11590581556317053213, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_formats.py": 18080627205081311773, + "venv/lib/python3.13/site-packages/PIL/ImageSequence.py": 16459886231802763032, + "venv/lib/python3.13/site-packages/_pytest/threadexception.py": 10464996327276961035, + "venv/lib/python3.13/site-packages/packaging/markers.py": 11602308203217831543, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunc_config.py": 14728417910466146460, + "venv/lib/python3.13/site-packages/cffi/_cffi_errors.h": 1479575326813300292, + "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.py": 7352635707860427145, + "frontend/build/_app/immutable/nodes/12.B7q-tXSO.js": 5591260477204622200, + "backend/src/plugins/translate/orchestrator_sql_rows.py": 13473935462270832541, + "venv/lib/python3.13/site-packages/attr/validators.py": 4453080662115911998, + "venv/lib/python3.13/site-packages/click/_compat.py": 14451519072991457993, + "venv/lib/python3.13/site-packages/pip/_internal/operations/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/automap.py": 4672057253309608464, + "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/__init__.py": 4870269250323682576, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.pyi": 13110894861187422750, + "venv/lib/python3.13/site-packages/yaml/resolver.py": 6669674521776449098, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py": 1422768761337195135, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_size.py": 16658280641516916790, + "venv/lib/python3.13/site-packages/PIL/ExifTags.py": 6869879826623916830, + "venv/lib/python3.13/site-packages/pyasn1/codec/native/decoder.py": 15336592439379775543, + "venv/lib/python3.13/site-packages/pandas/io/excel/_odswriter.py": 17220892094904423676, + "backend/src/plugins/translate/preview_prompt_builder.py": 2354938052590494405, + "venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py": 106471890743211548, + "venv/lib/python3.13/site-packages/pygments/styles/friendly.py": 13799024964803741891, + "venv/lib/python3.13/site-packages/pip/_internal/models/pylock.py": 16697828625021519459, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2cmap.py": 5863341586534531594, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_multi_thread.py": 5660352470978621226, + "backend/tests/services/clean_release/test_demo_mode_isolation.py": 1970652424726919519, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_array_from_pyobj.py": 9596224861290975079, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/plugin.py": 16042078006552521613, + "venv/lib/python3.13/site-packages/gitdb/db/pack.py": 13460574575424209609, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_string_array.py": 8260883866404398980, + "backend/src/plugins/translate/orchestrator.py": 2899451611486868631, + "venv/lib/python3.13/site-packages/httpcore/_backends/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/anyio/_core/_fileio.py": 4507100568401692983, + "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft_umath.cpython-313-x86_64-linux-gnu.so": 16443444849116065961, + "venv/lib/python3.13/site-packages/numpy/random/__init__.pxd": 7355772669172093814, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/METADATA": 2053484635654179443, + "venv/lib/python3.13/site-packages/numpy/_core/umath.py": 6132761780113682667, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_threading.py": 6656552495241857519, + "venv/lib/python3.13/site-packages/pandas/_libs/testing.pyi": 16691791330088523786, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_scalar_compat.py": 10534925655070162459, + "frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js": 1250794495161202602, + "venv/lib/python3.13/site-packages/pydantic/v1/schema.py": 10008526732895436361, + "venv/lib/python3.13/site-packages/pandas/io/formats/css.py": 11288129826285529457, + "venv/lib/python3.13/site-packages/_pytest/nodes.py": 515939386237558331, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sample.py": 16877779557071324115, + "backend/tests/test_auth.py": 6167020718035755646, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/METADATA": 13633829318759752611, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_fiscal.py": 14652343698825214870, + "venv/lib/python3.13/site-packages/pygments/lexers/ampl.py": 4594260378653369985, + "backend/alembic/versions/c4a3a2f74bfe_add_missing_columns_needs_review_.py": 4320606555167902916, + "backend/src/core/cot_logger.py": 3113579786931231764, + "venv/lib/python3.13/site-packages/fastapi/security/oauth2.py": 1925394177931547776, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_log_render.py": 18076931145915444473, + "venv/lib/python3.13/site-packages/pydantic/_internal/_git.py": 8760779092674434712, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/yaml/nodes.py": 3463622922914760653, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/array.py": 3517673819082574078, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_indexing.py": 16623126293364224089, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_formats.py": 1265319125235346745, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_aarch64_gcc.h": 8140470539757491899, + "venv/lib/python3.13/site-packages/pyasn1/compat/integer.py": 7365270971777550573, + "venv/lib/python3.13/site-packages/certifi/__init__.py": 9045580998764948967, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_setops.py": 10550603120090683350, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py": 8859757563354666575, + "venv/lib/python3.13/site-packages/pip/_internal/models/scheme.py": 4101322315594430874, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/dependency.py": 7300594038182867460, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py": 15060063324909302240, + "venv/lib/python3.13/site-packages/gitdb/test/test_pack.py": 7518388297247535189, + "venv/lib/python3.13/site-packages/cffi/ffiplatform.py": 6782148110863549472, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/default_comparator.py": 47118577003622736, + "venv/lib/python3.13/site-packages/jsonschema/cli.py": 9813914882090789748, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/events.py": 10224214051422678828, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/printing.py": 8681519649669408795, + "venv/lib/python3.13/site-packages/pandas/core/groupby/numba_.py": 15997665748332235683, + "venv/lib/python3.13/site-packages/yaml/tokens.py": 2279161238859232338, + "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators_v1.py": 16723743964226526155, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/RECORD": 12598309574903741388, + "frontend/build/_app/immutable/chunks/BwABSOzi.js": 12485550378293032886, + "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.py": 13012775703413938576, + "frontend/build/_app/immutable/chunks/hPO6SNG0.js": 8027434811274919386, + "backend/src/core/utils/fileio.py": 5783081738676692209, + "backend/src/plugins/translate/preview.py": 12468944154723146020, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_free.f90": 10939657740734987454, + "backend/git_repos/remote/test-repo.git/objects/5d/9eb555c4cac2d3ffdba84c07682b154fe4180d": 10756202165146702691, + "venv/lib/python3.13/site-packages/httpx/_api.py": 6697370469329778523, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/requests.py": 6703055041957748929, + "venv/lib/python3.13/site-packages/certifi/core.py": 7472627735517449185, + "venv/lib/python3.13/site-packages/cffi/error.py": 8147505969912539538, + "venv/lib/python3.13/site-packages/attr/_next_gen.py": 16353246941851015420, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/assertion.py": 5287436128235488464, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_install.py": 7181005402818830655, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dtype.py": 5336980126536664360, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/stata.py": 7249891970174867363, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/pandas/io/formats/__init__.py": 18140261896107421679, + "venv/lib/python3.13/site-packages/pydantic/config.py": 1450073446492183677, + "venv/lib/python3.13/site-packages/pandas/api/interchange/__init__.py": 181911269762882069, + "venv/lib/python3.13/site-packages/fastapi/cli.py": 13972351579061154035, + "venv/lib/python3.13/site-packages/apscheduler/executors/gevent.py": 17122637996105344344, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.pyi": 2101854548535776875, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/__init__.py": 371829636800613137, + "venv/lib/python3.13/site-packages/pandas/plotting/_misc.py": 10202078355301731720, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_append.py": 17016125916761365423, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/cache_key.py": 11118819065204404106, + "venv/lib/python3.13/site-packages/yaml/events.py": 745772917931702910, + "venv/lib/python3.13/site-packages/pygments/lexers/_stan_builtins.py": 1961875544235948177, + "frontend/src/lib/components/reports/ReportsList.svelte": 15460090587871611258, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/RECORD": 8971487317739420905, + "venv/lib/python3.13/site-packages/anyio/_core/_synchronization.py": 7050001375718876608, + "venv/lib/python3.13/site-packages/keyring/compat/__init__.py": 7578020229796085753, + "venv/lib/python3.13/site-packages/pandas/core/arrays/string_arrow.py": 16997970607998635570, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_common.py": 2265736743367879051, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py": 1197579313959289096, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_cipheralgorithm.py": 10057637461647348368, + "venv/lib/python3.13/site-packages/_pytest/pytester.py": 5423287282155948827, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_function.py": 644452398244782315, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayprint.pyi": 2424868797160722318, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/__init__.py": 8632038387100622009, + "venv/lib/python3.13/site-packages/jeepney/tests/test_wrappers.py": 4813328996575597980, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/getlimits.py": 7842717073693731093, + "venv/lib/python3.13/site-packages/pygments/lexers/berry.py": 4540479522141160472, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/json.py": 15373167408891671190, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nth.py": 17151595277666975085, + "frontend/build/_app/immutable/chunks/D2rm1xQZ.js": 2575467210086845899, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24662.f90": 4241310200087270990, + "venv/lib/python3.13/site-packages/passlib/ext/django/__init__.py": 11713862539944289631, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py": 11393911418463808195, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_ranges.py": 2572903460327860111, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_re.py": 1296579393600719224, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/lilypond.py": 1088755646568511197, + "venv/lib/python3.13/site-packages/fastapi/params.py": 14263655860695381481, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py": 3042005457138956499, + "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/__init__.py": 14268033439436797364, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_repr.py": 7552782621374439569, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_produces_warning.py": 750781117570375777, "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/INSTALLER": 17282701611721059870, - "frontend/build/_app/immutable/chunks/CSKg3QTJ.js": 13514833655854300178, - "venv/lib/python3.13/site-packages/requests/api.py": 12339139862411341381, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_month.py": 14597470286884431085, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_dtype.py": 12210524152630532747, - "venv/lib/python3.13/site-packages/sqlalchemy/types.py": 15819795572817819102, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_is_monotonic.py": 12769599198035357753, + "venv/lib/python3.13/site-packages/pygments/lexers/soong.py": 10273453675034306814, + "venv/lib/python3.13/site-packages/pygments/lexers/oberon.py": 15220143799603451830, + "venv/lib/python3.13/site-packages/pandas/core/computation/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/packaging/_tokenizer.py": 7442392076059866359, + "backend/src/services/__tests__/test_llm_prompt_templates.py": 17912427710321938757, + "venv/lib/python3.13/site-packages/pip/_internal/network/download.py": 5256731341219268693, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/block_docstring/foo.f": 2489756567993745076, + "venv/lib/python3.13/site-packages/pandas/core/config_init.py": 16537122488227845090, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mutable.py": 9323088483998280629, + "venv/lib/python3.13/site-packages/PIL/FtexImagePlugin.py": 8196606399875185931, + "scripts/generate_financial_arrears_data.py": 15989529185799374517, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.pyi": 6796322258258182775, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_feather.py": 13613400750755530364, + "venv/lib/python3.13/site-packages/fastapi/exceptions.py": 12665775976182239091, + "venv/lib/python3.13/site-packages/more_itertools/more.py": 1631135582441184133, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iat.py": 3570464615146398178, + "venv/lib/python3.13/site-packages/jeepney/io/threading.py": 525625939662612746, + "venv/lib/python3.13/site-packages/PIL/BmpImagePlugin.py": 16682567630621529903, + "frontend/src/lib/stores/__tests__/test_taskDrawer.js": 13582869875796637016, + "backend/src/core/task_manager/event_bus.py": 15798578020529126889, + "backend/tests/core/test_migration_engine.py": 7419155921858935534, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py": 2300277198409164253, + "venv/lib/python3.13/site-packages/annotated_doc/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numerictypes.pyi": 15443101937711720707, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_timedelta64.py": 16821719402662529276, + "venv/lib/python3.13/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz": 17626265752554406355, + "frontend/src/routes/settings/EnvironmentsTab.svelte": 1326318857426265025, + "backend/src/services/dataset_review/clarification_engine.py": 13640221555439415988, + "frontend/src/routes/dashboards/__tests__/dashboard-profile-override.integration.test.js": 15198772446275187033, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/content": 16257335642847993866, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_category.py": 3944898528119364040, + "venv/lib/python3.13/site-packages/pydantic/root_model.py": 20625057828723317, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_localize.py": 18409231586451494605, + "venv/lib/python3.13/site-packages/pip/_internal/exceptions.py": 2252826732781519645, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/pickleable.py": 7712040860601343075, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_fileno.py": 15541504502174047167, + "backend/src/services/profile_utils.py": 3954310438409803447, + "venv/lib/python3.13/site-packages/jeepney/tests/test_bus.py": 4260827599660763641, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_convert_dtypes.py": 5005259275646154894, + "venv/lib/python3.13/site-packages/pandas/core/arrays/base.py": 664515424699746697, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_gcc.h": 3561524604274567339, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/color.py": 15062855556511243610, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_indexing.py": 5063545640498065297, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/__init__.py": 293004106962245584, + "venv/lib/python3.13/site-packages/pandas/_testing/compat.py": 10172779814048259850, + "venv/lib/python3.13/site-packages/httpcore/_api.py": 16499985295426060327, + "venv/lib/python3.13/site-packages/greenlet/TPythonState.cpp": 12828594265749606443, + "venv/lib/python3.13/site-packages/pygments/lexers/markup.py": 10311057264960923433, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_freq_code.py": 13758353172962109140, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/segment.py": 2555584120510872966, + "venv/lib/python3.13/site-packages/anyio/_core/_subprocesses.py": 7271614043537119143, + "venv/lib/python3.13/site-packages/pygments/lexers/julia.py": 13516407944275342328, + "venv/lib/python3.13/site-packages/git/index/base.py": 641556304637379636, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_pickle.py": 6232650893735715362, + "venv/lib/python3.13/site-packages/pydantic_core/__init__.py": 10563134104783445328, + "venv/lib/python3.13/site-packages/numpy/ma/README.rst": 13203411621071845216, + "venv/lib/python3.13/site-packages/urllib3/connection.py": 12761371748350595804, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_interpolate.py": 7354685880215444946, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/__init__.py": 13749066369790839334, + "venv/lib/python3.13/site-packages/cryptography/x509/certificate_transparency.py": 11337143186866439208, + "venv/lib/python3.13/site-packages/pydantic/functional_serializers.py": 15285359935059345032, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayprint.py": 13335063050941743734, + "venv/lib/python3.13/site-packages/pydantic/_internal/_generics.py": 16438159042939059717, + "backend/_convert_defs.py": 208535643309836247, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/RECORD": 11604408378998955169, + "venv/lib/python3.13/site-packages/starlette/responses.py": 14081178833432955042, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/base.py": 6348888156686452825, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_function_base.pyi": 10972502518588022706, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5-fcafa220.so.3.3": 5620628698712971642, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/adapters.py": 2224938661429637508, + "venv/lib/python3.13/site-packages/rapidfuzz/__init__.py": 12462153783886855740, + "venv/lib/python3.13/site-packages/pandas/_libs/missing.pyi": 13543139288840944411, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_routines.py": 4860173574365060471, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/sql.py": 13315906751371033170, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py": 6109201994963211857, + "venv/lib/python3.13/site-packages/numpy/testing/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/git/config.py": 1724894144987553201, + "backend/tests/test_translate_history.py": 14504549443376034614, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/incfile.f90": 4348588746493647387, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_scrypt.py": 8106466515670064013, + "venv/lib/python3.13/site-packages/PIL/FliImagePlugin.py": 13364963053197422923, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/poolmanager.py": 5071697837617938064, + "venv/lib/python3.13/site-packages/pandas/core/computation/eval.py": 3176226283265954028, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_info.py": 10615285239043745712, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/RECORD": 1164466882742493155, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_join.py": 1697622393195784944, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_month.py": 8485374160446988786, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_construction.py": 6564829381134214401, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_neighborhood_iterator_imp.h": 10634781863364525895, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_html.py": 12040484325988635032, + "venv/lib/python3.13/site-packages/PIL/ImageEnhance.py": 3218405223202808764, + "venv/lib/python3.13/site-packages/pygments/formatters/rtf.py": 5850574956253747036, + "venv/lib/python3.13/site-packages/pygments/lexers/mosel.py": 1877592344012288438, + "venv/lib/python3.13/site-packages/pygments/__init__.py": 14493583771211110135, + "frontend/build/_app/immutable/chunks/Dq0Z2AGi.js": 3340623466096415766, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/mypy.py": 823265492261272631, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/scope.py": 4096190764836489091, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo.f": 5690596225005490325, + "venv/lib/python3.13/site-packages/click/exceptions.py": 9734955259447933725, + "venv/lib/python3.13/site-packages/gitdb/db/__init__.py": 2615504918450276693, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/METADATA": 14158024963562115116, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_cpp_exception.py": 11851283355840567373, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_loc.py": 17839380711970027363, + "backend/src/api/routes/translate/__init__.py": 6357999798616465776, + "frontend/build/_app/immutable/nodes/2.yxuT765B.js": 7527468193506722147, + "venv/lib/python3.13/site-packages/numpy/tests/test_configtool.py": 8208415015478205502, + "backend/src/services/notifications/service.py": 7032100258837946310, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress.py": 5951745251426856398, + "venv/lib/python3.13/site-packages/numpy/_core/lib/libnpymath.a": 17555816778997028283, + "venv/lib/python3.13/site-packages/pandas/tests/strings/conftest.py": 12065119814157197510, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/__init__.py": 7062045842767697645, + "venv/lib/python3.13/site-packages/apscheduler/executors/base.py": 1980520974511822101, + "venv/lib/python3.13/site-packages/jeepney/auth.py": 10565040329507087892, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odswriter.py": 5134385709728943904, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/persistence.py": 17908437937265384125, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_openpyxl.py": 9542840527483618681, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/LICENSE": 11167201188673981085, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/common.py": 11384497819737275399, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_overlap.py": 4202716587079637350, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/LICENSE": 8177430849046795595, + "backend/src/models/task.py": 10701108436197477339, + "backend/tests/core/test_defensive_guards.py": 1048600814279591148, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_style.py": 6849613206971398029, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategies.py": 15673926142454922581, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/panel.py": 17832701246248000383, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/WHEEL": 2357997949040430835, + "backend/src/core/encryption_key.py": 10697135200011385036, + "backend/src/plugins/translate/preview_response_parser.py": 15690759478655959205, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_isin.py": 11330491145602008796, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/RECORD": 11541505360973709787, + "venv/lib/python3.13/site-packages/pygments/lexers/nimrod.py": 8457135214131458774, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_hashtable.py": 7762405492052106717, + "backend/src/services/clean_release/__tests__/test_stages.py": 15353751625742444422, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_subclassing.py": 17318983445227658412, + "venv/lib/python3.13/site-packages/starlette/__init__.py": 3252794766897205870, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/typing_inspection/introspection.py": 13916695705179237047, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_symbolic.py": 8093576026032854125, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.cpython-313-x86_64-linux-gnu.so": 12654188486128938170, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_find_common_type.py": 9581516171321396333, + "venv/lib/python3.13/site-packages/git/repo/fun.py": 9019364012746153312, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_export_format.py": 17862222951776199694, + "venv/lib/python3.13/site-packages/pydantic/errors.py": 3506829204186508192, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_between_time.py": 14266333058913745751, + "frontend/src/lib/api/assistant.js": 15470667170083225911, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/util.py": 10640907133050862176, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/align.py": 6078666505647454830, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.py": 652332294655046106, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/python.py": 17886036670342116501, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.py": 15657897994221955882, + "venv/lib/python3.13/site-packages/pygments/lexers/webmisc.py": 9530962803076253849, + "venv/lib/python3.13/site-packages/rsa/key.py": 4783269051050521475, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/common_with_division.f": 13517753924061311074, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_app.py": 14788764260762890368, + "venv/lib/python3.13/site-packages/pyasn1/codec/der/encoder.py": 4790267438222037867, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_datetimelike.py": 10294379314868108649, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/padding.py": 13763154154862871596, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.cpython-313-x86_64-linux-gnu.so": 1137453907225619959, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/statreload.py": 5018382301336802445, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/constants.pyi": 4415188581374004936, + "frontend/src/lib/ui/Input.svelte": 8632025378593263475, + "frontend/build/_app/immutable/chunks/BoOsFBJ0.js": 11874060117929105318, + "venv/lib/python3.13/site-packages/pygments/lexers/procfile.py": 3860651022061090738, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/gevent.py": 4811840397266785551, + "venv/lib/python3.13/site-packages/numpy/random/mtrand.pyi": 17144590106028661128, + "venv/lib/python3.13/site-packages/pandas/core/reshape/concat.py": 2218848863149192467, + "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py": 6332087444349093252, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_replace.py": 4953527309376861139, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_kwarg.py": 10417724544218303733, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/horizontal_shard.py": 17615732108308858890, + "venv/lib/python3.13/site-packages/PIL/BlpImagePlugin.py": 7209218638257731375, + "frontend/static/favicon.png": 16740551781088792605, + "frontend/src/lib/components/dataset-review/CompiledSQLPreview.svelte": 4866254539173481462, + "backend/tests/test_resource_hubs.py": 3990299394875930767, + "venv/lib/python3.13/site-packages/attr/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/yaml/reader.py": 9873075843010693205, + "venv/lib/python3.13/site-packages/websockets/speedups.cpython-313-x86_64-linux-gnu.so": 10941658763526595989, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arrow_compat.py": 12986187285988432338, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.cpython-313-x86_64-linux-gnu.so": 18139875206371157315, + "venv/lib/python3.13/site-packages/dateutil/tz/win.py": 8427821225251397364, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_chaining_and_caching.py": 11784574265874608859, + "venv/lib/python3.13/site-packages/sqlalchemy/inspection.py": 8563914460959280101, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_extract.py": 14547819021277949280, + "venv/lib/python3.13/site-packages/jsonschema/tests/typing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_replace.py": 15992095120302643273, + "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.pyi": 17903213294220335051, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_duplicated.py": 6659959750254523427, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isetitem.py": 14895552405828068177, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_util.py": 16908393806993809474, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_factorize.py": 5585211699185937865, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkeyutils-dfe70bd6.so.1.5": 9414227432578104677, + "frontend/src/components/tools/DebugTool.svelte": 17267560034225794881, + "backend/src/api/routes/__init__.py": 7861712296282676167, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_legend.py": 13094583566764796297, + "venv/lib/python3.13/site-packages/pygments/lexers/math.py": 1647133236144311250, + "venv/lib/python3.13/site-packages/numpy/random/bit_generator.cpython-313-x86_64-linux-gnu.so": 2574035311375507054, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string.py": 3834067244989779682, + "venv/lib/python3.13/site-packages/fastapi/security/http.py": 8465565118555441408, + "venv/lib/python3.13/site-packages/pydantic/experimental/__init__.py": 492399139963056461, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufuncs.pyi": 14995898537484305307, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_indexing.py": 8943385096811168079, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_formats.py": 3525283725314253763, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_setitem.py": 15731267129055947469, + "venv/lib/python3.13/site-packages/passlib/utils/pbkdf2.py": 15802959653228956404, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/asn1.pyi": 16048852379621461316, + "backend/src/plugins/debug.py": 2035734953696955393, + "backend/src/plugins/translate/orchestrator_planner.py": 9681702657993216474, + "venv/lib/python3.13/site-packages/rapidfuzz/utils.py": 5682627611354455144, + "venv/lib/python3.13/site-packages/passlib/tests/_test_bad_register.py": 5446848310854472376, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_ccalendar.py": 8819357623337533489, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/repr.py": 839417422880281122, + "frontend/src/components/llm/DocPreview.svelte": 25645090129568414, + "frontend/src/components/tasks/TaskResultPanel.svelte": 7735278333136205649, + "venv/lib/python3.13/site-packages/pydantic/warnings.py": 5142352784799639092, + "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.py": 3001896031623204147, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_arithmetic.py": 13320788210970132239, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_array.py": 16867692337986161626, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/WHEEL": 5076688779572520166, + "venv/lib/python3.13/site-packages/pygments/lexers/savi.py": 95400050302239274, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_dtypes.py": 2313400030798822026, + "venv/lib/python3.13/site-packages/numpy/__init__.pyi": 15820654525292895396, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_api.py": 14120915875574640262, + "venv/lib/python3.13/site-packages/websockets/client.py": 5229500079651465772, + "venv/lib/python3.13/site-packages/pandas/core/ops/mask_ops.py": 16152832446926420035, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py": 5951912505933673835, + "venv/lib/python3.13/site-packages/rpds/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/bitwise_ops.pyi": 15598239133239520456, + "venv/lib/python3.13/site-packages/numpy/fft/__init__.pyi": 10354990065626209946, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_first_valid_index.py": 15763400665851423183, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_at_time.py": 15352346149584081864, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_msvc.h": 14580207065003376324, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_overlaps.py": 8287716909793575347, + "venv/lib/python3.13/site-packages/pip/_internal/network/cache.py": 10606522397693824641, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_initialstub_already_started.py": 5562030563802006923, + "venv/lib/python3.13/site-packages/pydantic/v1/main.py": 213087273729911492, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_constructors.py": 979131987837682435, + "venv/lib/python3.13/site-packages/numpy/testing/__init__.pyi": 8126257788928282891, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/ecdsa/eddsa.py": 15966858338456571506, + "venv/lib/python3.13/site-packages/git/objects/tag.py": 7954808535585493300, + "venv/lib/python3.13/site-packages/pygments/lexers/teal.py": 10004159499175716139, + "venv/lib/python3.13/site-packages/pygments/lexers/ml.py": 7510453371068494792, + "venv/lib/python3.13/site-packages/websockets/datastructures.py": 5180442040351387281, + "venv/lib/python3.13/site-packages/pygments/lexers/capnproto.py": 15713281507099411738, + "venv/lib/python3.13/site-packages/keyring/__main__.py": 14602212239605295451, + "venv/lib/python3.13/site-packages/pygments/lexers/diff.py": 5930189593946259160, + "venv/bin/pip3": 13861749540792881808, + "venv/lib/python3.13/site-packages/passlib/utils/compat/_ordered_dict.py": 14248790502300368024, + "venv/lib/python3.13/site-packages/httpcore/_ssl.py": 6665087294869504820, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/resolution.py": 5132658725516243311, + "frontend/e2e/tests/translation.e2e.js": 6244274143080191568, + "frontend/src/lib/components/dataset-review/__tests__/source_intake_panel.ux.test.js": 13445861056598482363, + "venv/lib/python3.13/site-packages/rsa/util.py": 13423880475585138638, + "backend/src/core/migration/risk_assessor.py": 1061977005072298440, + "venv/lib/python3.13/site-packages/six.py": 948867418687428421, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_editable.py": 12494407279914732338, + "backend/src/plugins/translate/__tests__/test_dictionary_filter.py": 12872406014365116097, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/utils.py": 3710235316419562274, + "venv/lib/python3.13/site-packages/jsonschema/_format.py": 8987698648383779883, + "backend/src/plugins/translate/_run_source.py": 11569137727611732019, + "backend/src/models/dataset_review_pkg/_profile_models.py": 3121807680272913669, + "backend/conftest.py": 16521922883434363388, + "venv/lib/python3.13/site-packages/PIL/ImtImagePlugin.py": 17599267755504817169, + "venv/lib/python3.13/site-packages/sqlalchemy/util/concurrency.py": 1325673503812009695, + "backend/src/schemas/translate.py": 4202862152769241553, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/LICENSE": 12688029424398428498, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs7.pyi": 2777456956260371820, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/operators.py": 5584982192355398255, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_non_compound.f90": 8048650253889213460, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/jwt_bearer.py": 6973859422068490274, "venv/lib/python3.13/site-packages/apscheduler/schedulers/qt.py": 12828696264909048448, + "venv/lib/python3.13/site-packages/pandas/core/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_keys.py": 10813898277355150779, + "venv/lib/python3.13/site-packages/pygments/lexers/yang.py": 7409365668425173823, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/numpy/ma/testutils.py": 9355402483340137461, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/datasource.pyi": 80441145804744162, + "venv/lib/python3.13/site-packages/pandas/core/computation/pytables.py": 16326122536715961388, + "dist/docker/ss-tools.current.tar.xz": 12481237908945663094, + "venv/lib/python3.13/site-packages/keyring/backend_complete.zsh": 14339032329370660432, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_datetime64.py": 16272266806084058743, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_concat.py": 4472503149811617440, + "venv/lib/python3.13/site-packages/PIL/ImageCms.py": 5610897002957128546, + "venv/lib/python3.13/site-packages/pygments/lexers/grammar_notation.py": 9009644754106884095, + "venv/lib/python3.13/site-packages/pydantic/_internal/_mock_val_ser.py": 12265320534388472158, + "venv/lib/python3.13/site-packages/fastapi/types.py": 3867821246650502267, + "venv/lib/python3.13/site-packages/numpy/_array_api_info.pyi": 13283968924732680159, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/conftest.py": 13388202260795998613, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd_module.py": 7122050193390298178, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/meson.build": 9642189398934130247, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/dtype_api.h": 17040779421165903019, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_constructors.py": 17392287676196409733, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/conftest.py": 2643915042664055969, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_asyncio.py": 2820168745710754136, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py": 5908944827805063108, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_fillna.py": 13675584998644907265, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/METADATA": 9458019064618635438, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/parameters.py": 5104015814292812973, + "venv/lib/python3.13/site-packages/fastapi/temp_pydantic_v1_params.py": 9436036880518047720, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_dtypes.py": 481191723198819568, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_gcs.py": 7733247456252965901, + "venv/lib/python3.13/site-packages/PIL/_imagingmath.cpython-313-x86_64-linux-gnu.so": 77725273398858074, + "venv/lib/python3.13/site-packages/git/refs/symbolic.py": 14996822770181935589, + "venv/lib/python3.13/site-packages/pygments/lexers/_julia_builtins.py": 8846111188795315103, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/RECORD": 173225030102092319, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py": 5995058808539530576, + "venv/lib/python3.13/site-packages/numpy/f2py/rules.pyi": 1342380029484127983, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/api.py": 12339139862411341381, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraypad.pyi": 12178766981280242620, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_compat.h": 6512969159139431485, + "venv/lib/python3.13/site-packages/pygments/lexers/wowtoc.py": 17527844575999013592, + "frontend/src/types/dashboard.ts": 9718666955722367566, + "frontend/src/lib/api/translate/corrections.js": 3619404083693055292, + "frontend/src/routes/admin/users/+page.svelte": 4464001185352807842, + "frontend/src/components/storage/FileList.svelte": 11661146151896000393, + "backend/src/schemas/__tests__/test_settings_and_health_schemas.py": 13526616847756581689, + "frontend/build/_app/immutable/chunks/4Mt7E5DY.js": 1876154373397748488, + "frontend/src/routes/datasets/review/ReviewWorkspaceLeftSidebar.svelte": 9009994851177005358, + "backend/src/services/clean_release/__tests__/test_source_isolation.py": 17714198919084455758, + "venv/lib/python3.13/site-packages/itsdangerous/exc.py": 11181421963755661477, + "venv/lib/python3.13/site-packages/_pytest/config/exceptions.py": 3199245296954654462, + "venv/lib/python3.13/site-packages/pandas/util/version/__init__.py": 3844072588696074141, + "venv/lib/python3.13/site-packages/tzlocal/py.typed": 15130871412783076140, + "frontend/build/_app/immutable/chunks/DJ7pJ0kI.js": 5528949065320899171, + "venv/lib/python3.13/site-packages/passlib/tests/test_apps.py": 10454136535890937237, + "backend/src/core/utils/superset_context_extractor/_templates.py": 6790136732701012567, + "backend/src/plugins/translate/__tests__/test_preview.py": 2014051431239347014, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_ios.h": 6751259472945638998, + "venv/lib/python3.13/site-packages/jaraco/classes/ancestry.py": 11742194518360056470, + "venv/lib/python3.13/site-packages/packaging/_elffile.py": 9546225990833172494, + "backend/src/api/routes/translate/_run_list_routes.py": 18118346695557887371, + "venv/lib/python3.13/site-packages/passlib/handlers/ldap_digests.py": 8658529184965699766, + "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.cpython-313-x86_64-linux-gnu.so": 17652852308919396403, + "frontend/src/routes/profile/__tests__/profile-settings-state.integration.test.js": 3674649277514557450, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation": 4826455560768665215, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel.py": 7728646523045538484, + "venv/lib/python3.13/site-packages/pip/_internal/utils/glibc.py": 12335359492756627313, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_ewm.py": 12153690217678546798, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_api.py": 13319208721442328400, + "venv/lib/python3.13/site-packages/pip/_internal/network/__init__.py": 1586148254475575526, + "venv/lib/python3.13/site-packages/httpcore/py.typed": 15130871412783076140, + "frontend/build/_app/immutable/chunks/ikwS1dR6.js": 3267599179274813039, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_openssl.py": 10446441599800965961, + "venv/lib/python3.13/site-packages/pip/_internal/utils/direct_url_helpers.py": 6075890513530296650, + "venv/lib/python3.13/site-packages/idna/codec.py": 4260241549062101779, + "venv/lib/python3.13/site-packages/pip/_internal/utils/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/__init__.py": 15130871412783076140, + "frontend/src/routes/settings/DatabaseConnectionsTab.svelte": 834010895835527896, + "backend/src/services/clean_release/repositories/audit_repository.py": 9617813121624417458, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/binding.py": 17078280153372356796, + "venv/lib/python3.13/site-packages/passlib/handlers/postgres.py": 14162382430051693313, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_shift.py": 9952689832827833247, + "venv/lib/python3.13/site-packages/pip/_internal/commands/__init__.py": 3795218871212151351, + "venv/lib/python3.13/site-packages/pydantic/v1/decorator.py": 6124748023704700724, + "venv/lib/python3.13/site-packages/passlib/tests/__main__.py": 16477180140297957650, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/formatters/terminal256.py": 11473927781167132440, + "frontend/src/components/git/GitOperationsPanel.svelte": 5140779139910068800, + "backend/tests/conftest.py": 14115149798000023183, + "frontend/src/lib/auth/__tests__/permissions.test.js": 13555349356008297952, + "venv/lib/python3.13/site-packages/jaraco/context/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authenticate_client.py": 16886645731942472285, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/console.py": 10217327186846649646, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/conftest.py": 17897502508251413988, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_resolution.py": 18198858926948468112, + "venv/lib/python3.13/site-packages/pip/_internal/models/candidate.py": 3099785363867247334, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/models.py": 9513172366009392193, + "backend/src/api/routes/assistant/_schemas.py": 6735060826070731731, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/exclusions.py": 15852672500377116297, + "venv/lib/python3.13/site-packages/pygments/lexers/graphics.py": 13783143864729682444, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/licenses/LICENSE": 3868190977070717994, + "venv/lib/python3.13/site-packages/pydantic/_migration.py": 17982816821930887921, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test__iotools.py": 6260912730310453037, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_timer.py": 8196003526646080498, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py": 10505346210114745037, + "venv/lib/python3.13/site-packages/referencing/tests/test_retrieval.py": 10782087416909177703, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/functions.py": 16567226076808693246, + "venv/lib/python3.13/site-packages/numpy/f2py/rules.py": 9322914583460630877, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_datetime.py": 1678885152993000592, + "venv/lib/python3.13/site-packages/pygments/lexers/functional.py": 15896849233007178031, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/emscripten_fetch_worker.js": 10530710876604536879, + "backend/src/api/routes/assistant/__init__.py": 14959302162073618175, + "venv/lib/python3.13/site-packages/numpy/exceptions.py": 10981418339663118022, + "venv/lib/python3.13/site-packages/pydantic/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/response.py": 15004896908941411698, + "venv/lib/python3.13/site-packages/pandas/errors/__init__.py": 13444371880185740908, + "venv/lib/python3.13/site-packages/pandas/tests/construction/test_extract_array.py": 3100523370314150013, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA": 18410956221497090898, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/pytestplugin.py": 1591034895268415944, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/__init__.py": 15130871412783076140, + "backend/src/models/mapping.py": 7003804560728225253, + "venv/lib/python3.13/site-packages/pip/_internal/cli/main_parser.py": 7942770969280622910, + "frontend/build/_app/immutable/nodes/17.D2vhX5As.js": 17828979217052834606, + "venv/bin/pyrsa-encrypt": 5858278204727417482, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_complex.py": 11732242536199439023, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_from_dummies.py": 5670811108950300761, + "venv/lib/python3.13/site-packages/packaging/pylock.py": 15346942039643310253, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_interval.py": 2911379732293827726, + "frontend/e2e/fixtures/auth.fixture.js": 12330457928638392604, + "venv/lib/python3.13/site-packages/jsonschema_specifications/__init__.py": 5732822354953774657, + "venv/lib/python3.13/site-packages/pydantic/v1/dataclasses.py": 7833468513057242440, + "venv/lib/python3.13/site-packages/passlib/utils/binary.py": 13123975436674239306, + "frontend/build/_app/immutable/chunks/CSKg3QTJ.js": 13514833655854300178, + "backend/src/plugins/llm_analysis/__tests__/test_client_headers.py": 5527172390260032966, + "venv/lib/python3.13/site-packages/gitdb/db/base.py": 3375449584942578385, + "backend/tests/test_task_manager.py": 1706217361689506763, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.BSD": 3858254215169937333, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-np0-objarr.npy": 1615546603475261360, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows_renderer.py": 1076842273781909680, + "venv/lib/python3.13/site-packages/charset_normalizer/cli/__main__.py": 2148876100501691980, + "venv/lib/python3.13/site-packages/pandas/util/_print_versions.py": 15326663505771588740, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine_first.py": 3100691245280812910, + "venv/lib/python3.13/site-packages/sqlalchemy/event/legacy.py": 13465911776953636949, + "venv/lib/python3.13/site-packages/pygments/lexers/thingsdb.py": 6073447481627395832, + "frontend/src/lib/components/layout/__tests__/sidebarNavigation.test.js": 8459006438402290167, + "frontend/src/components/llm/ValidationReport.svelte": 11810571826279890996, + "backend/src/services/reports/__tests__/test_report_service.py": 6045116534626984913, + "venv/lib/python3.13/site-packages/apscheduler/executors/asyncio.py": 2230610508870117058, + "venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py": 5076219228736706283, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arraymethod.py": 14421188237635557797, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/response.py": 9594165377270752433, + "venv/lib/python3.13/site-packages/pip/_internal/models/wheel.py": 10473287225032326206, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_assignability.pyi": 1718610984396277997, + "venv/lib/python3.13/site-packages/uvicorn/lifespan/off.py": 16254524872582859561, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/WHEEL": 7795541085444298627, + "venv/lib/python3.13/site-packages/jsonschema/_typing.py": 13152346021901861822, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_repr.py": 16609300578827398344, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_easter.py": 3397863170068576196, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/__init__.py": 15487893533770969571, + "venv/lib/python3.13/site-packages/pycparser/c_parser.py": 3954138286651284327, + "venv/lib/python3.13/site-packages/pygments/lexers/configs.py": 7712797939104760185, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_asfreq.py": 6162659555626011770, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py": 15130871412783076140, + "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js": 17118608261683412409, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/stride_tricks.pyi": 8257977532799807847, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_calendar.py": 12313641252472031631, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/token_endpoint.py": 6118770923176099889, + "venv/lib/python3.13/site-packages/pandas/core/interchange/from_dataframe.py": 8278998381856028462, + "venv/lib/python3.13/site-packages/websockets/sync/server.py": 7966978001578584901, + "venv/lib/python3.13/site-packages/pip/_internal/cli/autocompletion.py": 7651919329635563739, + "venv/lib/python3.13/site-packages/numpy/_core/_simd.cpython-313-x86_64-linux-gnu.so": 12609666221355091416, + "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.py": 503331864022631297, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_as_unit.py": 8492383355919959393, + "backend/src/core/async_superset_client.py": 17097960069181151584, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937.py": 7299586490442539723, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/queue.py": 7820997545569991670, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nbit_base_example.pyi": 8571452382806614924, + "venv/lib/python3.13/site-packages/pandas/tests/test_optional_dependency.py": 7930709646653025355, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_fillna.py": 4610666845291315333, + "venv/lib/python3.13/site-packages/requests/sessions.py": 6936640856092320928, + "venv/lib/python3.13/site-packages/PIL/GimpPaletteFile.py": 2040459799887417392, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/random.py": 12722750410117529986, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py": 6510864297510700136, + "venv/lib/python3.13/site-packages/pandas/_testing/_io.py": 14264259716673715185, + "venv/lib/python3.13/site-packages/cffi/_embedding.h": 9441285871823018603, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-2.csv": 7063857529719738394, + "venv/lib/python3.13/site-packages/rapidfuzz/_common_py.py": 15410157970973169427, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_unittests.py": 18157503533859249155, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py": 7624316342162916928, + "venv/lib/python3.13/site-packages/numpy/_typing/_char_codes.py": 14162745886150625150, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/jose/backends/cryptography_backend.py": 2875270748627611635, + "venv/lib/python3.13/site-packages/numpy/fft/tests/test_helper.py": 5362180645056413158, + "venv/lib/python3.13/site-packages/passlib/tests/test_context_deprecated.py": 7876919198015151018, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked_shared.py": 8453174300922339015, + "venv/lib/python3.13/site-packages/PIL/_webp.cpython-313-x86_64-linux-gnu.so": 11070012119632935103, + "frontend/src/components/tools/ConnectionForm.svelte": 4060811448046342875, + "backend/src/api/routes/assistant/_dataset_review_dispatch.py": 10108558091463171824, + "venv/lib/python3.13/site-packages/PIL/EpsImagePlugin.py": 1824020964179364491, + "venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py": 3893922370396791826, + "backend/git_repos/remote/test-repo.git/objects/ae/8d2f5dec5d421b2d6cc1eebaad6d2ef901a90d": 15877648001251681939, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ufuncobject.h": 5112456258078662871, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate_regression.py": 17425202198134409689, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_django.py": 8455054295577203929, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_dialect.py": 4700488504252803159, + "venv/lib/python3.13/site-packages/httpx/_main.py": 1812303705973265093, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/WHEEL": 2823347510103459191, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.pyi": 16976386782787920450, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_values.py": 8372572954663628844, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex_like.py": 1407890639206728014, + "venv/lib/python3.13/site-packages/iniconfig/__init__.py": 12623265899068631092, + "venv/lib/python3.13/site-packages/pandas/core/frame.py": 2630807744775247533, + "venv/lib/python3.13/site-packages/git/objects/submodule/base.py": 10140490301897269395, + "backend/src/plugins/translate/preview_session_serializer.py": 8901687711861433007, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/installed.py": 18153165899841229597, + "venv/lib/python3.13/site-packages/attrs/converters.py": 13109003765349201809, + "venv/lib/python3.13/site-packages/rsa/randnum.py": 13958355153634534526, + "venv/lib/python3.13/site-packages/_pytest/_code/__init__.py": 6211607282116942091, + "venv/lib/python3.13/site-packages/numpy/core/_multiarray_umath.py": 10973016138581934035, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/datetimelike_accumulations.py": 14719894187829729175, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resampler_grouper.py": 8388490036390951341, + "venv/bin/jsonschema": 16516374846693491442, + "venv/lib/python3.13/site-packages/pip/_internal/req/constructors.py": 17485271035479378662, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_shape_manipulation.py": 12598431412396627042, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_get_numeric_data.py": 5896318696417090902, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_series.py": 10922287546912569276, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/interface.py": 3134071530627935964, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/config.py": 10707678943298497676, + "backend/git_repos/remote/test-repo.git/hooks/sendemail-validate.sample": 8058833260784074657, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_convert.py": 17957207974799052653, + "venv/lib/python3.13/site-packages/_pytest/unraisableexception.py": 4597178803320514675, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_msvc.h": 2465089365642299646, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_polynomial.pyi": 4266795948450921502, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_iloc.py": 16160558158155038350, + "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.cpython-313-x86_64-linux-gnu.so": 17856848989827220388, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py": 10455718110956856820, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_sorting.py": 10523740688498427929, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/operators.py": 2468046993343796128, + "venv/lib/python3.13/site-packages/numpy/fft/_helper.py": 15906444100774706395, + "venv/lib/python3.13/site-packages/starlette/routing.py": 16063812008141874150, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/file_proxy.py": 17695135735211897444, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_reductions.py": 1713831062142840146, + "venv/lib/python3.13/site-packages/PIL/GdImageFile.py": 16875784236394545058, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/pygments/lexers/_stata_builtins.py": 18053452765493556047, + "frontend/src/routes/datasets/review/+page.svelte": 12909115647298935839, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE": 4274653168153720331, + "venv/lib/python3.13/site-packages/cryptography/x509/oid.py": 13909847799728532602, + "backend/src/api/routes/__tests__/test_dashboards.py": 14899529124453053148, + "venv/lib/python3.13/site-packages/pandas/io/json/_json.py": 1393095374713367445, + "backend/src/plugins/translate/__tests__/test_executor.py": 14442607617569532284, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_axis.py": 4097534573847151853, + "venv/lib/python3.13/site-packages/numpy/_utils/__init__.pyi": 13530522898409268883, + "backend/src/services/clean_release/repositories/publication_repository.py": 11208425895213508165, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.cpython-313-x86_64-linux-gnu.so": 13104135217735363438, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/__init__.py": 9820770319106921495, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/WHEEL": 11630459739183915695, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem": 15624463015628939254, + "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.pyi": 2042169688753984918, + "venv/lib/python3.13/site-packages/pygments/styles/pastie.py": 15784474766258831882, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_rank.py": 5398520257852104723, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/errors.py": 6538052228921426956, + "backend/tests/services/clean_release/test_publication_service.py": 17277940125978730159, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_dropna.py": 3620959914439334511, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/resource_protector.py": 4823504113176750140, + "venv/lib/python3.13/site-packages/greenlet/TBrokenGreenlet.cpp": 10822436772777706192, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayobject.py": 13337664596833150958, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_iter.py": 9232021002336641851, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ma.pyi": 8186102816080813799, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/utils.py": 2959395751217827103, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_policy.py": 13856626567322091253, + "venv/lib/python3.13/site-packages/pandas/_libs/indexing.cpython-313-x86_64-linux-gnu.so": 13188040757764159731, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/RECORD": 15865206372525596795, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_cov_corr.py": 8117511855230249212, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_linux.h": 4071314837793906998, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/__init__.py": 348469143777130970, + "venv/lib/python3.13/site-packages/pydantic/_internal/_config.py": 12760390602912369447, + "venv/lib/python3.13/site-packages/rsa/pkcs1.py": 2576732594642491910, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/RECORD": 10264342250058307301, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-2.csv": 4052796393463357888, + "venv/lib/python3.13/site-packages/pygments/lexers/ptx.py": 4855144232064607995, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/anyio/streams/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_series.py": 17126297747681689408, + "venv/lib/python3.13/site-packages/_pytest/cacheprovider.py": 5114545353355728201, + "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.hpp": 6366432579621298231, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/__init__.py": 15644105878811738516, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dropna.py": 3168969662216766293, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_setops.py": 8622608042929141013, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py": 8659210689723088321, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/vector.py": 9941185556118343263, + "venv/lib/python3.13/site-packages/PIL/_imagingtk.cpython-313-x86_64-linux-gnu.so": 1708198947135304326, + "backend/src/core/utils/superset_context_extractor/__init__.py": 1074191108198498747, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/events.py": 13473858896579666058, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/__init__.py": 1798074863773727377, + "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/array.py": 1919242530821725197, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_typing.py": 17589387690638097975, + "venv/lib/python3.13/site-packages/pygments/lexers/futhark.py": 10102535873433759899, + "frontend/build/_app/immutable/chunks/BXeCyPl8.js": 3759367862764800149, + "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.pyi": 6072607880689125113, + "venv/lib/python3.13/site-packages/websockets/sync/router.py": 10335595955948355382, + "venv/lib/python3.13/site-packages/pandas/core/shared_docs.py": 2173578869755325502, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_impl.py": 16610770935842519289, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_extension_array_equal.py": 16119106336497630796, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/__init__.py": 697601512486720554, + "backend/src/plugins/translate/orchestrator_retry.py": 9287851697930332790, + "backend/src/services/clean_release/__tests__/test_report_builder.py": 17265459530244848426, + "venv/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py": 991029278889407558, + "venv/lib/python3.13/site-packages/urllib3/filepost.py": 3814725067598042920, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libsasl2-883649fd.so.3.0.0": 13306917506408577991, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_clip.py": 13498704233745189154, + "venv/bin/activate.fish": 5275961582873298085, + "venv/lib/python3.13/site-packages/websockets/headers.py": 13587146967490088657, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/typst.tpl": 5055419073126793137, + "venv/lib/python3.13/site-packages/pandas/_libs/algos.cpython-313-x86_64-linux-gnu.so": 15706331654215762588, + "venv/bin/pyrsa-verify": 6762739642629904442, + "venv/lib/python3.13/site-packages/pandas/io/excel/_base.py": 1251545894005373734, + "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/test_array_with_attr.py": 6568496539994758985, + "venv/lib/python3.13/site-packages/cryptography/x509/verification.py": 1079694784860492682, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/lambdas.py": 13377298868598623228, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/__init__.py": 2246704695838251846, + "frontend/build/_app/immutable/chunks/B1H1HSGu.js": 17342622807666537448, + "backend/git_repos/remote/test-repo.git/objects/a1/3525f5f44e26d8fbec3226bd488d14793575a2": 3742572048019652365, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/licenses/LICENSE": 8438117004702803716, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/padding.py": 11244071766694094554, + "backend/tests/test_smoke_plugins.py": 10704454611361370435, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_einsum.py": 17286384318722237180, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/argon2.py": 15389474901714096890, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/METADATA": 732041908491732594, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_timestamp.py": 12253372102602993034, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_extension.py": 17245018204352082978, + "venv/lib/python3.13/site-packages/tzlocal/unix.py": 995019116408448073, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename.py": 4890779493250245903, + "venv/lib/python3.13/site-packages/pydantic/deprecated/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/websockets/protocol.py": 6401227903827084901, + "venv/lib/python3.13/site-packages/pandas/core/computation/parsing.py": 3084943521520476999, + "venv/lib/python3.13/site-packages/pydantic_core/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/query.py": 3867868366067025348, + "venv/lib/python3.13/site-packages/urllib3/http2/connection.py": 1661620860881397554, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/twodim_base.pyi": 10156504423258357540, + "venv/lib/python3.13/site-packages/numpy/_core/memmap.py": 4093510602708416858, + "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.pyi": 15864712173815045309, + "venv/lib/python3.13/site-packages/pygments/cmdline.py": 15628499088643246809, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_copy.py": 11335108864615756235, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate.py": 11205714006000849926, + "venv/lib/python3.13/site-packages/_pytest/_code/source.py": 9488112979192923739, + "venv/lib/python3.13/site-packages/numpy/fft/_helper.pyi": 9546918341939102632, + "venv/lib/python3.13/site-packages/pandas/core/reshape/reshape.py": 5387892304726762127, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log1p.csv": 16333455295913164408, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_memmap.py": 8709963808115492688, + "venv/lib/python3.13/site-packages/click/testing.py": 15973659312284678327, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_astype.py": 11386658862732393123, + "venv/lib/python3.13/site-packages/_pytest/logging.py": 10972553724755511468, + "venv/lib/python3.13/site-packages/pandas/_libs/reshape.pyi": 88721900806893992, + "venv/lib/python3.13/site-packages/PIL/CurImagePlugin.py": 17697303205012181529, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_period.py": 10309599277099605395, + "venv/lib/python3.13/site-packages/psycopg2/errors.py": 10441360078612452560, + "backend/src/services/auth_service.py": 17642785219801916280, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/gcp.py": 12328409672947966867, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_dists.py": 16746363359320577809, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/WHEEL": 9788895026336324100, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/__config__.pyi": 1422114139050700402, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/signals.py": 10390443713463805090, + "venv/lib/python3.13/site-packages/pillow.libs/libwebpmux-7f11e5ce.so.3.1.2": 13105940093474848953, + "venv/lib/python3.13/site-packages/pandas/core/tools/numeric.py": 8510988912136285022, + "venv/lib/python3.13/site-packages/pydantic/v1/annotated_types.py": 12925825196690299531, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_fillna.py": 16297834601116528166, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numerictypes.py": 8836108334624807647, + "venv/lib/python3.13/site-packages/numpy/_globals.pyi": 12839879732484756979, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_subclass.py": 5540948578269213810, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.cpython-313-x86_64-linux-gnu.so": 12473903676666680134, + "venv/lib/python3.13/site-packages/httpcore/_sync/__init__.py": 7676937747094511752, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/AUTHORS": 3558407083075395848, + "frontend/src/lib/ui/PageHeader.svelte": 3800784212388586861, + "frontend/src/routes/settings/+page.svelte": 8601836419948434099, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/setuponly.py": 10959102379908419762, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_loadtxt.py": 17562784940619942447, + "venv/lib/python3.13/site-packages/sqlalchemy/types.py": 15819795572817819102, + "frontend/tailwind.config.js": 13139381085024634801, + "venv/lib/python3.13/site-packages/tzlocal/__init__.py": 11017072814026497364, + "backend/src/api/routes/__tests__/test_assistant_authz.py": 4130274102090064437, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_numba.py": 1668304729617150075, + "venv/lib/python3.13/site-packages/numpy/_core/multiarray.py": 4484782354376574355, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/dtype.pyi": 465999539608816848, + "venv/lib/python3.13/site-packages/attrs/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/box.py": 14303526978791498662, + "venv/lib/python3.13/site-packages/pip/_vendor/__init__.py": 11734104142988812647, + "venv/lib/python3.13/site-packages/pydantic/networks.py": 15767095796055034509, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dlpack.py": 7908102652282922952, + "venv/lib/python3.13/site-packages/urllib3/__init__.py": 1854778274753799815, + "frontend/src/routes/settings/LlmSettings.svelte": 11336336338708911991, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/introspection.py": 5447179017910128771, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_unique.py": 3148372351781648868, + "venv/lib/python3.13/site-packages/pandas/core/strings/__init__.py": 13025387151657797130, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_astype.py": 15275720655739710249, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_floats.py": 3064615530495290792, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/client.py": 18443944554871937480, + "venv/lib/python3.13/site-packages/pydantic/deprecated/tools.py": 16466182352694741629, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_cython_aggregations.py": 10818895573637904764, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/RECORD": 2688648742701423549, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_period.py": 6047041810867211174, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/__init__.py": 3577295959173028042, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/__init__.py": 11009955319062089477, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_orc.py": 1248059610437937895, + "venv/lib/python3.13/site-packages/pandas/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/io/formats/info.py": 215303194287834907, + "venv/lib/python3.13/site-packages/PIL/WebPImagePlugin.py": 5378349556563611996, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/authorization_server.py": 17967809814827547139, + "venv/lib/python3.13/site-packages/httpcore/_async/http_proxy.py": 13364803556177997526, + "frontend/build/_app/immutable/nodes/36.ezLH2TGV.js": 8634796778012649005, + "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.py": 4037074830103951000, + "backend/src/api/routes/__tests__/test_assistant_api.py": 16438212076178324033, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py": 2628895559914914693, + "venv/lib/python3.13/site-packages/starlette/convertors.py": 17839747154274354605, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_stack.py": 5316826578695144945, + "venv/lib/python3.13/site-packages/pydantic/_internal/_serializers.py": 12077180322353709930, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/signature.py": 3614545297259098566, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/RECORD": 9974324769263969827, + "venv/lib/python3.13/site-packages/pandas/core/internals/__init__.py": 1377108808392195313, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_analytics.py": 4510712334676848202, + "venv/lib/python3.13/site-packages/smmap/buf.py": 18231775424238352734, + "venv/lib/python3.13/site-packages/jeepney/tests/test_auth.py": 15125055415702968414, + "venv/lib/python3.13/site-packages/pandas/core/internals/concat.py": 12151675567885981464, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_overlaps.py": 2328652590379935311, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_util.py": 13997184496924925110, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/elements.py": 11185011314498348980, + "venv/lib/python3.13/site-packages/uvicorn/server.py": 15714008348292243019, + "venv/lib/python3.13/site-packages/pydantic/v1/datetime_parse.py": 10467114625921924095, + "venv/lib/python3.13/site-packages/pygments/lexers/rita.py": 8105458520706587972, + "venv/lib/python3.13/site-packages/dotenv/main.py": 7408889670575271484, + "venv/lib/python3.13/site-packages/pygments/lexers/webidl.py": 8097260111959278117, + "backend/src/plugins/translate/dictionary_entries.py": 11394786590056973146, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.py": 17538029557373507617, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/__init__.py": 3521833849634327520, + "venv/lib/python3.13/site-packages/fastapi/openapi/models.py": 1680354178147364813, + "venv/lib/python3.13/site-packages/git/objects/base.py": 11498407361286170399, + "backend/src/api/routes/git/_repo_operations_routes.py": 5819532622410034169, + "backend/src/services/clean_release/stages/base.py": 344723354852893815, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/sphinxext.py": 2403717774564798878, + "venv/lib/python3.13/site-packages/websockets/legacy/http.py": 16266924390281399720, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo90.f90": 2435648607844815947, + "backend/src/dependencies.py": 9481683691730170837, + "venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py": 13206297306768491360, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_timedeltas.py": 4118671843063545221, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape_base.pyi": 10291383658131961346, + "venv/lib/python3.13/site-packages/numpy/_core/records.py": 11414828908088050950, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py": 6152202474302791232, + "venv/lib/python3.13/site-packages/pygments/lexers/_csound_builtins.py": 14530739818491398298, + "venv/lib/python3.13/site-packages/pandas/tests/generic/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_dtypes.py": 6831042272921908068, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/authorization_code.py": 4958605540994687737, + "venv/lib/python3.13/site-packages/pygments/lexers/graphql.py": 2307854666015224677, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_indexing.py": 546024765756472196, + "backend/src/models/connection.py": 7467905739729526898, + "venv/lib/python3.13/site-packages/requests/exceptions.py": 6474239743209354989, + "venv/lib/python3.13/site-packages/ecdsa/rfc6979.py": 11564966030899162026, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/recarray_from_file.fits": 15116318600514274152, + "venv/lib/python3.13/site-packages/pandas/core/indexes/frozen.py": 1419685664276640120, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_update.py": 12293424926920594944, + "venv/lib/python3.13/site-packages/pygments/lexers/_vbscript_builtins.py": 9882134558469491349, + "backend/src/plugins/translate/__tests__/test_inline_correction.py": 3644785257160613130, + "venv/lib/python3.13/site-packages/pandas/io/excel/_xlsxwriter.py": 16873939324418752965, + "venv/lib/python3.13/site-packages/pycparser/lextab.py": 16045770806700381661, + "backend/tests/services/clean_release/test_policy_resolution_service.py": 3471271299977247598, + "backend/git_repos/remote/test-repo.git/refs/heads/master": 12485529318667335076, + "venv/lib/python3.13/site-packages/apscheduler/triggers/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_jsonschema_test_suite.py": 12733761462141495972, + "venv/lib/python3.13/site-packages/pydantic/deprecated/class_validators.py": 2200511838255373813, + "venv/lib/python3.13/site-packages/numpy/core/getlimits.py": 14257687540182552408, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/WHEEL": 7454950858448014158, + "frontend/src/pages/Dashboard.svelte": 3653104499351855122, + "frontend/src/lib/components/dataset-review/__tests__/validation_findings_panel.ux.test.js": 8994492423600483794, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_grouping.py": 5994909693214299971, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/LICENSE": 8423899413955829149, + "venv/lib/python3.13/site-packages/websockets/streams.py": 15786581670248099415, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__init__.py": 8811477458865810434, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_frame_equal.py": 12459816298158655274, + "venv/lib/python3.13/site-packages/pygments/lexers/parsers.py": 12372594608244797844, + "frontend/src/components/backups/BackupManager.svelte": 15237659305406789107, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/licenses/LICENSE": 6808958893921859106, + "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.pyi": 84606125837677197, + "venv/lib/python3.13/site-packages/PIL/_imagingmorph.cpython-313-x86_64-linux-gnu.so": 11171810899664471137, + "venv/lib/python3.13/site-packages/keyring/backends/libsecret.py": 11621088556661567306, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/emath.pyi": 3718540478840842664, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop.py": 17940792593116540139, + "frontend/build/_app/immutable/chunks/Dx5pMPrM.js": 12135044835772226705, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/basereload.py": 14479857245734482609, + "venv/lib/python3.13/site-packages/passlib/utils/md4.py": 4521366665754877351, + "venv/lib/python3.13/site-packages/idna/uts46data.py": 5698404064763638591, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_bool.py": 433342705586552936, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/min_max_.py": 13457611930368041821, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_delete.py": 5958440386632506587, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp2.csv": 10950201682596097452, + "venv/lib/python3.13/site-packages/pandas/core/groupby/grouper.py": 18110568057754450953, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_selectable_constructors.py": 84919308502240880, + "venv/lib/python3.13/site-packages/pygments/styles/default.py": 13992125975366109635, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/__init__.py": 4337679908168515343, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_size.py": 8527839563560550806, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_delitem.py": 16538190861492829338, + "venv/lib/python3.13/site-packages/pygments/styles/autumn.py": 8750937312198920971, + "venv/lib/python3.13/site-packages/pandas/core/groupby/indexing.py": 18009437216172136227, + "backend/src/api/routes/__tests__/test_git_api.py": 43259826036955162, + "venv/lib/python3.13/site-packages/_pytest/assertion/__init__.py": 16392348866405910618, + "backend/src/api/routes/__tests__/test_migration_routes.py": 4357515184436771537, + "backend/src/plugins/translate/__tests__/test_orthogonal_fixes.py": 15839990777181837988, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_astype.py": 4854809596041987380, + "backend/src/services/clean_release/stages/data_purity.py": 10459527906317168920, + "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.pyi": 9206355685192026844, + "backend/git_repos/remote/test-repo.git/hooks/pre-rebase.sample": 15693225890270078290, + "venv/lib/python3.13/site-packages/gitdb/test/test_example.py": 3039934065817753241, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/default_styles.py": 4469204219504675080, + "venv/lib/python3.13/site-packages/pandas/core/arrays/__init__.py": 10460719978124302175, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/_mapping.py": 11752660241391232497, + "frontend/src/lib/ui/Icon.svelte": 2201964532717139995, + "backend/git_repos/remote/test-repo.git/objects/4f/4e0958b3a42eff67a2b8cfd084d05c43e54716": 6040513978194442637, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_na_indexing.py": 8712547126522523899, + "backend/src/services/reports/normalizer.py": 11408466232944555336, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_setops.py": 15365064654930699428, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_compare.py": 3079569478684646491, + "backend/src/services/clean_release/demo_data_service.py": 1822246829322620623, + "backend/src/services/mapping_service.py": 9713400496587758211, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_dict_compat.py": 18196543090455290455, + "venv/lib/python3.13/site-packages/pip/_internal/commands/completion.py": 4435231315212798769, + "venv/lib/python3.13/site-packages/pydantic/v1/networks.py": 13262073155118055511, + "venv/lib/python3.13/site-packages/pandas/core/missing.py": 13332302791063948992, + "venv/lib/python3.13/site-packages/pip/_internal/network/lazy_wheel.py": 12936988990566680659, + "frontend/build/_app/immutable/nodes/13.vtGvGltt.js": 4586863421425982153, + "venv/lib/python3.13/site-packages/jeepney/io/tests/conftest.py": 13381752264278264218, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_processors.py": 16557117595879208702, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_type_check.py": 3935091606923106753, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_numpy.py": 9663734591667845940, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_assign.py": 2647902466315482406, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpp": 4590574206187223887, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_csv.py": 10737444103906389448, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/accumulate.py": 5780225603819849877, + "venv/lib/python3.13/site-packages/pandas/core/common.py": 14578909424716153057, + "venv/lib/python3.13/site-packages/python_multipart/multipart.py": 5677735019292537174, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayterator.py": 3076316311545674897, + "venv/lib/python3.13/site-packages/pytest/__init__.py": 6137272437722350673, + "venv/lib/python3.13/site-packages/sqlalchemy/schema.py": 16029542421991709659, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/util.py": 18279366777506483176, + "venv/lib/python3.13/site-packages/passlib/ext/django/utils.py": 8118896525895185626, + "backend/src/plugins/storage/__init__.py": 5046554941488079204, + "venv/lib/python3.13/site-packages/httpx/__version__.py": 9473642134297828811, + "venv/lib/python3.13/site-packages/PIL/MpegImagePlugin.py": 7726254767528154514, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/__init__.py": 1948986914452928735, + "venv/lib/python3.13/site-packages/sqlalchemy/event/registry.py": 2857989070743633709, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/RECORD": 3205266407680661753, + "venv/lib/python3.13/site-packages/_pytest/python.py": 1829073958095716309, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/dtype.py": 9342837412280377086, + "venv/lib/python3.13/site-packages/h11/_abnf.py": 10456996134895429201, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_all_methods.py": 18227706645906325527, + "frontend/src/routes/tools/storage/+page.svelte": 2651964385884112547, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24008.f": 3872245077994936272, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/win64python2.npy": 11005857803794805939, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_indexing.py": 4334578480714520407, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/io.py": 11072022722163954973, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py": 11917862648584782420, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/aioodbc.py": 2018840826888846321, + "backend/src/models/dataset_review_pkg/_mapping_models.py": 6232602161916984422, + "venv/lib/python3.13/site-packages/pygments/regexopt.py": 16073437303824961930, + "venv/lib/python3.13/site-packages/git/objects/fun.py": 15014209679156260865, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/passlib/tests/test_apache.py": 12515001998770565375, + "venv/lib/python3.13/site-packages/pygments/__main__.py": 13200500591994479045, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/WHEEL": 12146818530001975731, + "venv/lib/python3.13/site-packages/websockets/legacy/__init__.py": 12332512329533591682, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_numpyconfig.h": 18150055639397773687, + "venv/lib/python3.13/site-packages/greenlet/__init__.py": 3447613556981242475, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_to_numpy.py": 14845850776625255794, + "backend/src/api/routes/__tests__/test_clean_release_api.py": 11565478953155085157, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.cpython-313-x86_64-linux-gnu.so": 9014806524727977333, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/METADATA": 3618776395694990076, + "backend/src/services/reports/report_service.py": 17586405613867657190, + "venv/lib/python3.13/site-packages/pydantic/datetime_parse.py": 196119761382305574, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_fields.py": 16222559851336781264, + "backend/src/services/llm_provider.py": 9619956052011769991, + "backend/git_repos/remote/test-repo.git/hooks/pre-applypatch.sample": 15598058327720909861, + "venv/lib/python3.13/site-packages/authlib/oauth2/client.py": 7237359250450222680, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_replace.py": 18039668944453454689, + "backend/tests/test_translate_jobs.py": 16585154497956900995, + "venv/lib/python3.13/site-packages/authlib/deprecate.py": 3563885283090710443, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polyutils.pyi": 3841660741524427413, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_sparse.py": 14343335524508450058, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_algos.py": 5356470832688354593, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/types.py": 16795741817129898714, + "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.pyi": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_categorical.py": 12897027063618569795, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_indexing.py": 3841182456456779012, + "venv/lib/python3.13/site-packages/pygments/lexers/installers.py": 5083244110640899732, + "frontend/src/lib/stores/environmentContext.js": 1762535408632658714, + "backend/src/api/routes/reports.py": 5647380957044157360, + "backend/src/models/dataset_review_pkg/_execution_models.py": 9810012962062422666, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_recfunctions.py": 16014503284544747867, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-1.csv": 16349400027062875699, + "venv/lib/python3.13/site-packages/cryptography/exceptions.py": 5173540025160237758, + "venv/lib/python3.13/site-packages/psycopg2/_ipaddress.py": 12332672835751787869, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/types.py": 1343652083039949282, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_indexing.py": 14552860427542050771, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_functions.py": 16376319335326812580, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby.py": 16180421154734310873, + "venv/lib/python3.13/site-packages/pygments/lexers/prql.py": 1159462562014166646, + "backend/src/services/clean_release/manifest_service.py": 10873340336255220063, + "venv/lib/python3.13/site-packages/pandas/core/computation/common.py": 5369352684655954608, + "backend/src/plugins/llm_analysis/scheduler.py": 14673660738877638091, + "venv/lib/python3.13/site-packages/pillow.libs/libpng16-4a38ea05.so.16.53.0": 13136089038268964667, + "venv/lib/python3.13/site-packages/pandas/core/reshape/merge.py": 300205528811693975, + "dist/docker/ss-tools.1.1.0.tar.xz": 14627825683119220800, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/descriptor_props.py": 7044852927393650707, + "backend/src/core/auth/config.py": 8065625913451826937, + "venv/lib/python3.13/site-packages/fastapi/requests.py": 14587947416691824903, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_repr.py": 11737029058271003097, + "venv/lib/python3.13/site-packages/pyasn1/type/namedtype.py": 1859990775875190116, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period_range.py": 9598872514187524706, + "backend/src/api/routes/dashboards/__init__.py": 15214026957520044598, + "venv/lib/python3.13/site-packages/authlib/integrations/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/meson.build.template": 9935732744553086652, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/METADATA": 3814104793342833872, + "venv/lib/python3.13/site-packages/pandas/tseries/holiday.py": 1134358927599488799, + "venv/lib/python3.13/site-packages/urllib3/util/connection.py": 15511685694589961094, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_counting.py": 5628494712451120258, + "venv/lib/python3.13/site-packages/dateutil/parser/__init__.py": 11377431154251655738, + "venv/lib/python3.13/site-packages/httpx/_urlparse.py": 8270294590945652412, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh18335.f90": 1126438609189294255, + "venv/lib/python3.13/site-packages/starlette/middleware/authentication.py": 15211131264936212938, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/licenses/LICENSE": 3996324383113221529, + "venv/lib/python3.13/site-packages/packaging/requirements.py": 2896326326372719999, + "venv/lib/python3.13/site-packages/pandas/tests/extension/__init__.py": 15130871412783076140, + "backend/src/plugins/llm_analysis/service.py": 5318741399446187226, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/metadata.py": 1362166613223529108, + "venv/lib/python3.13/site-packages/numpy/lib/user_array.py": 14003456655595459585, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/boxplot.py": 7090586111222952862, + "backend/src/services/dataset_review/event_logger.py": 8083530196989488371, + "venv/lib/python3.13/site-packages/pip/_internal/index/__init__.py": 14829074315307489628, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/cookies.py": 4997226307650350876, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77comments.f": 4801926170672147659, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_astype.py": 6578539240745332772, + "venv/lib/python3.13/site-packages/_pytest/_code/code.py": 5248836309618416369, + "venv/lib/python3.13/site-packages/_pytest/_py/path.py": 8464061867075119556, + "venv/lib/python3.13/site-packages/idna/__init__.py": 6489437464172324468, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/comparisons.pyi": 9338084426044982038, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_warnings.py": 13071210233876253749, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_missing.py": 7034102519042351965, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/bulk_persistence.py": 2307818727114646866, + "venv/lib/python3.13/site-packages/httpcore/_async/connection.py": 2155744160496556069, + "venv/lib/python3.13/site-packages/gitdb/base.py": 7927279446353696534, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/RECORD": 10552906886624188222, + "venv/lib/python3.13/site-packages/jaraco/classes/properties.py": 6976928770004942858, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_period_index.py": 6634414375581820058, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_arithmetic.py": 15071337112968886738, + "venv/lib/python3.13/site-packages/pygments/styles/xcode.py": 14124729477757765388, + "frontend/build/_app/immutable/chunks/YCgcezOr.js": 10021400156442571831, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/idna/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_dict.py": 4937173601709369119, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_concat.py": 3788461606817482165, + "venv/lib/python3.13/site-packages/urllib3/util/proxy.py": 17667887856427739069, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptography.py": 4383854709657749855, + "venv/lib/python3.13/site-packages/jsonschema/__init__.py": 6925762200139836824, + "venv/lib/python3.13/site-packages/pillow.libs/libharfbuzz-0692f733.so.0.61230.0": 23110287346167671, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-1.csv": 9129730764673472086, + "venv/lib/python3.13/site-packages/pandas/_testing/__init__.py": 10127496080404419481, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_api_info.pyi": 7887489694871300469, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/__init__.py": 2436195108273216959, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_rewrite_warning.py": 4895919872954538758, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/unix.py": 13808187640638677050, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/appengine.py": 16194997685865850655, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_take.py": 1216902953519121176, + "venv/lib/python3.13/site-packages/pyasn1/type/univ.py": 7163437624197683039, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_chained_assignment_deprecation.py": 7694137945143129474, + "venv/lib/python3.13/site-packages/pycparser/ply/ygen.py": 5520340773074632724, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/RECORD": 7914594107698271488, + "frontend/build/_app/immutable/chunks/RH3HxiAM.js": 15426109189074801931, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py": 15130871412783076140, + "backend/src/api/routes/health.py": 4494840518139445701, + "venv/lib/python3.13/site-packages/pip/_internal/index/collector.py": 7454652626329467900, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_concat.py": 16549010224568175202, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py": 12184194085993665945, + "venv/lib/python3.13/site-packages/authlib/jose/__init__.py": 317573635113539563, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/__init__.py": 16146480198665866916, + "venv/lib/python3.13/site-packages/pandas/_libs/properties.pyi": 9268470215750269699, + "backend/src/core/plugin_loader.py": 245217431000027879, + "venv/lib/python3.13/site-packages/authlib/oauth2/auth.py": 8455258275234626534, + "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/npymath.ini": 1071371630695963467, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/base.py": 3628510830969664111, + "venv/lib/python3.13/site-packages/h11/__init__.py": 222801976520197308, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_fillna.py": 7349777469648334282, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_errors.py": 3301889847545579393, + "venv/lib/python3.13/site-packages/pygments/lexers/fantom.py": 1986459352773446836, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/util.py": 5252158959761945519, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py": 10367325397209016951, + "venv/lib/python3.13/site-packages/starlette/websockets.py": 13834731343216764204, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/licenses/LICENSE": 5563356551734678258, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.cpython-313-x86_64-linux-gnu.so": 1211491156495529145, + "venv/lib/python3.13/site-packages/numpy/testing/overrides.py": 11819432487830192978, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_datetimeindex.py": 14816894350435545024, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcom_err-2abe824b.so.2.1": 12870528025628871295, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/hooks.py": 8580161128344266243, + "venv/lib/python3.13/site-packages/pydantic/deprecated/copy_internals.py": 1245428320513062166, + "venv/lib/python3.13/site-packages/numpy/matlib.py": 112839038090033740, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_indexing.py": 2274965639578580723, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/mean_.py": 3641008929910474457, + "venv/lib/python3.13/site-packages/pygments/styles/sas.py": 12363791134941577382, + "venv/lib/python3.13/site-packages/pygments/lexers/algebra.py": 9651893228421050246, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libk5crypto-b1f99d5c.so.3.1": 10669523137596317954, + "frontend/build/_app/immutable/chunks/CV7dXtfg.js": 15129819915415394723, + "frontend/build/_app/immutable/chunks/BoPlBqXU.js": 15621955836226522600, + "frontend/build/_app/immutable/chunks/1cp5Nfxo.js": 8461125705150579835, + "backend/src/core/migration_engine.py": 1303635539349402052, + "backend/src/plugins/translate/preview_token_estimator.py": 254295902082954386, + "venv/lib/python3.13/site-packages/PIL/ImageMorph.py": 13861961228125789866, + "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.pyi": 15745512451486888537, + "frontend/src/routes/translate/dictionaries/[id]/+page.svelte": 17719561272401762626, + "backend/src/api/routes/dataset_review_pkg/_routes.py": 11040245149296528009, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/diagnose.py": 17275977405061138181, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/multiarray.py": 3224049574922696945, + "backend/tests/test_logger.py": 11506419828617508917, + "venv/lib/python3.13/site-packages/anyio/__init__.py": 12752386096259972231, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_npy_units.py": 8966226962799105383, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/conftest.py": 12368472226536104037, + "venv/lib/python3.13/site-packages/httpcore/_async/http2.py": 16350817785499681224, + "venv/lib/python3.13/site-packages/git/cmd.py": 8854786163152637148, + "backend/src/services/clean_release/repositories/candidate_repository.py": 14784344453820731941, + "venv/lib/python3.13/site-packages/pip/_internal/index/sources.py": 11609272011565024668, + "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pyi": 15790738442952696034, + "venv/lib/python3.13/site-packages/pycparser/_build_tables.py": 569728086093219621, + "venv/lib/python3.13/site-packages/pygments/lexers/monte.py": 15158829991357994573, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_na_scalar.py": 13950899907067372014, + "backend/src/core/superset_client/_dashboards_filters.py": 11002883972618719697, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_types.py": 7175518113263603341, + "venv/lib/python3.13/site-packages/_pytest/reports.py": 13930709338396699351, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_asof.py": 8656532774394927388, + "backend/src/api/routes/__tests__/test_reports_api.py": 8830242634438523357, + "backend/git_repos/remote/test-repo.git/objects/79/b070ad8af7bbc12169f7660d2a7a72ec7a889a": 8153595714065675791, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/INSTALLER": 17282701611721059870, + "backend/src/models/dataset_review_pkg/_semantic_models.py": 6267455409655567066, + "venv/lib/python3.13/site-packages/anyio/_core/_tempfile.py": 230288423480899091, + "venv/lib/python3.13/site-packages/pillow.libs/libjpeg-32d42e18.so.62.4.0": 11717944412021329207, + "frontend/src/lib/api/translate/runs.js": 1220731879179347641, + "frontend/src/lib/components/translate/__tests__/test_correction_cell.svelte.js": 2314051239468628554, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/tags.py": 254599446317461188, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertsql.py": 4633484475514351889, + "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.pyi": 10487441680587530350, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.pyi": 17469792319379909312, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_ops.py": 14420421526781215500, + "venv/lib/python3.13/site-packages/pandas/core/arrays/categorical.py": 15993211255162930859, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_semicolon_split.py": 8153770165829451525, + "venv/lib/python3.13/site-packages/h11/_receivebuffer.py": 4631433251046338381, + "backend/src/services/reports/__tests__/test_type_profiles.py": 17262223295445285285, + "venv/lib/python3.13/site-packages/cffi/api.py": 5325821560978502135, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_row.py": 918740441973901148, + "frontend/src/components/TaskHistory.svelte": 7819646903874958430, + "backend/src/api/routes/environments.py": 1329328901500854319, + "backend/src/services/clean_release/__tests__/test_policy_engine.py": 9953185933720550640, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_table.tpl": 17198355067645888735, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.pyi": 16164841755339571545, + "venv/lib/python3.13/site-packages/jeepney/io/trio.py": 11859775321483330469, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_skew.py": 5276102682005383261, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_astype.py": 1961831758405941258, + "venv/lib/python3.13/site-packages/dateutil/tzwin.py": 5149446703746204349, + "venv/lib/python3.13/site-packages/fastapi/security/__init__.py": 1345944646061334046, + "venv/lib/python3.13/site-packages/pygments/lexers/elm.py": 14683437310498104916, + "backend/src/services/dataset_review/repositories/session_repository.py": 4058785536575367912, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_macos.py": 7028037591427052891, + "venv/lib/python3.13/site-packages/httpx/_content.py": 1301854837767395687, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_arithmetic.py": 16829312896275893002, + "venv/lib/python3.13/site-packages/_pytest/timing.py": 14080119171718125986, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/METADATA": 3786116428839780263, + "venv/lib/python3.13/site-packages/numpy/random/_common.cpython-313-x86_64-linux-gnu.so": 12834724130890991478, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_join.py": 1364886077926604427, + "venv/lib/python3.13/site-packages/pygments/util.py": 4815334059917765852, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_insert.py": 11635164392966270621, + "backend/src/schemas/__init__.py": 8700952869915187087, + "backend/src/services/git/_merge.py": 16183885992149357137, + "venv/lib/python3.13/site-packages/websockets/legacy/framing.py": 12759022769700230409, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/base.py": 13444129650880426353, + "frontend/build/_app/immutable/chunks/DepzS_bZ.js": 2243340878436625890, + "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.py": 2953876934840155920, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/REQUESTED": 15130871412783076140, + "backend/src/scripts/__init__.py": 5400372851656282343, + "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.pyi": 8195943561787777127, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufuncs.pyi": 1808147181379894521, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_loc.py": 2127584756555503873, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/METADATA": 7727489762869155085, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.pyi": 2700568594438441246, + "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/setupplan.py": 7496801885995567141, + "venv/lib/python3.13/site-packages/pandas/core/base.py": 10262717073058743378, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_c_parser_only.py": 3038797485572319804, + "frontend/src/lib/stores/__tests__/taskDrawer.test.js": 12307450726550947042, + "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.abi3.so": 8691731321358529264, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_dialect.py": 6294522482440261756, + "build.sh": 2979280474875068, + "venv/lib/python3.13/site-packages/jose/jws.py": 11592462993417182096, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/METADATA": 8128818962678047497, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.pyi": 3145980717589050713, + "venv/lib/python3.13/site-packages/pandas/core/computation/ops.py": 18250656489649699560, + "venv/lib/python3.13/site-packages/pandas/core/arraylike.py": 11439602957100017713, + "venv/lib/python3.13/site-packages/authlib/integrations/django_client/integration.py": 13610682193839598457, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/__init__.py": 9997757256282720103, + "venv/lib/python3.13/site-packages/multipart/multipart.py": 26386459953377432, + "venv/lib/python3.13/site-packages/pydantic/mypy.py": 10034028441785588819, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_diff.py": 2216748778018882333, + "venv/lib/python3.13/site-packages/pygments/plugin.py": 18006138132596504101, + "venv/lib/python3.13/site-packages/pygments/lexers/maple.py": 7402467573829618534, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/__main__.py": 2688850704829116818, + "backend/src/core/utils/superset_context_extractor/_base.py": 15730081123519745401, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_combine_concat.py": 3201068805128232483, + "venv/lib/python3.13/site-packages/secretstorage/item.py": 15486045386471912693, + "venv/lib/python3.13/site-packages/PIL/TarIO.py": 13651401276968618561, + "backend/src/api/routes/dashboards/_router.py": 8641007051243802224, + "backend/src/api/routes/git/_repo_routes.py": 7367823490321383813, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.BSD": 16833702494864671773, + "venv/lib/python3.13/site-packages/pandas/_libs/properties.cpython-313-x86_64-linux-gnu.so": 7092136701092358552, + "backend/src/services/clean_release/__tests__/test_manifest_builder.py": 9320699981861675127, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/scalars.py": 3468820466230647963, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_block_internals.py": 17935662848672401507, + "venv/lib/python3.13/site-packages/jaraco/functools/__init__.pyi": 2872039192528394677, + "backend/src/api/routes/git_schemas.py": 9243114525622127446, + "backend/alembic_test.db": 14995253579408279707, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo90.f90": 12508663906609241323, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_box_unbox.py": 7579587620530953711, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.pyi": 7410507092040239447, + "venv/lib/python3.13/site-packages/pygments/lexers/jsonnet.py": 1314044008440065525, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py": 5630511062945201327, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/provision.py": 8707620698060818654, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/attributes.py": 5068814626384132900, + "venv/lib/python3.13/site-packages/pygments/lexers/modeling.py": 15714077241620129689, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_parquet.py": 11050150015772440923, + "frontend/src/routes/settings/settings-utils.js": 4790951495955131309, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/pygments/lexers/_usd_builtins.py": 8671446144574174964, + "venv/lib/python3.13/site-packages/pandas/tests/computation/test_eval.py": 2921337784087808487, + "venv/lib/python3.13/site-packages/pygments/lexers/gleam.py": 5161116162065925222, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/isocintrin/isoCtests.f90": 6208802899442270399, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/licenses/LICENSE": 9235234428907927646, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpython-313-x86_64-linux-gnu.so": 7659849621107414188, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/misc/extended_precision.pyi": 6541258584648209609, + "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.pyi": 8879196542984811747, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_isoc.py": 4923640657296076239, + "venv/lib/python3.13/site-packages/attr/converters.pyi": 17675091788614033166, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_npfuncs.py": 13987318550267921389, + "venv/lib/python3.13/site-packages/pandas/api/types/__init__.py": 3995030354191071797, + "venv/lib/python3.13/site-packages/numpy/_core/overrides.pyi": 13121243505132110607, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/nested_secrets.py": 15691917061715429369, + "venv/lib/python3.13/site-packages/authlib/oauth1/client.py": 7314311571224771, + "backend/src/services/clean_release/manifest_builder.py": 5683864742902531449, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/style.py": 10665998898253460114, + "venv/lib/python3.13/site-packages/cryptography/hazmat/_oid.py": 755042927629281577, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pymssql.py": 6650008663791598065, + "venv/lib/python3.13/site-packages/pydantic/decorator.py": 6984214881365537811, + "venv/lib/python3.13/site-packages/PIL/_imaging.pyi": 17119084207765761921, + "venv/lib/python3.13/site-packages/dateutil/_common.py": 12492546559232079071, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_extint128.py": 11074657869101074458, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_logical_ops.py": 16166386974005224415, + "venv/lib/python3.13/site-packages/anyio/_core/_testing.py": 14024940365851109155, + "venv/lib/python3.13/site-packages/jeepney/tests/test_fds.py": 6257376331171761928, + "venv/lib/python3.13/site-packages/pydantic/v1/env_settings.py": 6194387033830177219, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_melt.py": 15663807247682146566, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_types.py": 4083297168618981717, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_legendre.py": 18266595775500927868, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/LICENSE": 12064892310002036996, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/device_code.py": 6712610002227849281, + "frontend/src/routes/+page.ts": 11160596731849880920, + "frontend/src/routes/reports/llm/[taskId]/report_page.contract.test.js": 4825093389030464765, + "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.pyi": 9629457630163613604, + "frontend/build/_app/immutable/chunks/DOxI2NAm.js": 11236647372068635428, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pct_change.py": 18056235438452125034, + "venv/lib/python3.13/site-packages/pandas/core/computation/expressions.py": 10684284531040203747, + "venv/lib/python3.13/site-packages/pandas/io/json/__init__.py": 12333505155070172626, + "venv/lib/python3.13/site-packages/dateutil/parser/_parser.py": 9443336890192151397, + "frontend/src/lib/stores/__tests__/mocks/stores.js": 14098417748659067309, + "venv/lib/python3.13/site-packages/click/_termui_impl.py": 3296085065310341855, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_truncate.py": 6478126371906156409, + "venv/lib/python3.13/site-packages/numpy/core/einsumfunc.py": 3484247123731380377, + "backend/src/plugins/translate/__tests__/test_dictionary_import.py": 7271610466914322056, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 12574929384355254457, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_resolution.py": 9651416588266430520, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_indexing.py": 11572911419313573163, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_pipe.py": 8255917221095334212, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/dictionary.py": 9599439750713575741, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/client.py": 17305914656270781911, + "venv/lib/python3.13/site-packages/fastapi/__init__.py": 11054203163075417954, + "venv/lib/python3.13/site-packages/pygments/styles/murphy.py": 8177992406082781615, + "backend/src/services/clean_release/repositories/report_repository.py": 17952038790457725175, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/constant_time.py": 2499026008403328245, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/RECORD": 17985800801793506250, + "venv/lib/python3.13/site-packages/pandas/_testing/contexts.py": 11714205921752896137, + "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimes.py": 14331984919364185787, + "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.pyi": 2186875333484796630, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_laguerre.py": 7475592201750602042, + "venv/lib/python3.13/site-packages/attr/_funcs.py": 5302917072419624193, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/__init__.py": 7316739139609490520, + "venv/lib/python3.13/site-packages/numpy/_utils/__init__.py": 7731065084036767848, + "venv/lib/python3.13/site-packages/pandas/io/xml.py": 326466573926392764, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py": 9925183787411796679, + "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.pyi": 14069586330277925998, + "venv/lib/python3.13/site-packages/pluggy/_tracing.py": 7276210024011809655, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/emoji.py": 9166774203918731896, + "frontend/src/lib/components/assistant/__tests__/assistant_confirmation.integration.test.js": 12582020574293031526, + "backend/src/services/clean_release/repositories/manifest_repository.py": 12395698948312301073, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/strings.pyi": 402863478592154500, + "venv/lib/python3.13/site-packages/pandas/compat/_constants.py": 3909166275649444824, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/RECORD": 12015288851076448205, + "venv/lib/python3.13/site-packages/pandas/_libs/algos.pyi": 22405377601029952, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_ndarray_backed.py": 10097812802469682233, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_arrow_compat.py": 17818294260710640443, + "venv/lib/python3.13/site-packages/httpcore/_backends/base.py": 5491727479625093183, + "backend/src/services/git/_remote_providers.py": 15093805025432566446, + "venv/lib/python3.13/site-packages/pip/__pip-runner__.py": 1138979712722199296, + "venv/lib/python3.13/site-packages/dateutil/easter.py": 14631233890122048084, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/string.f": 17328443345813567449, + "backend/src/services/rbac_permission_catalog.py": 4811877724682885443, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo90.f90": 3492659529812762861, + "venv/lib/python3.13/site-packages/pandas/plotting/_core.py": 6766059152026241747, + "frontend/src/routes/datasets/[id]/+page.svelte": 14911166036063098761, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/RECORD": 1051307407005554409, + "backend/src/plugins/translate/_llm_parse.py": 3278211416813203155, + "venv/lib/python3.13/site-packages/numpy/_typing/_array_like.py": 2161873439361281488, + "venv/lib/python3.13/site-packages/websockets/auth.py": 499195617702591757, + "venv/lib/python3.13/site-packages/pygments/lexers/graphviz.py": 15677908982985025500, + "venv/lib/python3.13/site-packages/fastapi/__main__.py": 10755252341326986079, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/modules.pyi": 131293606732000587, + "venv/lib/python3.13/site-packages/pydantic/plugin/_loader.py": 792663962985957671, + "venv/lib/python3.13/site-packages/numpy/_typing/__init__.py": 18242238390157333247, + "venv/lib/python3.13/site-packages/_pytest/_py/error.py": 2257336434271433543, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo77.f": 7353666717928699883, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/signals.py": 13677199634497673845, + "venv/lib/python3.13/site-packages/pip/_internal/commands/search.py": 8292218231992728142, + "venv/lib/python3.13/site-packages/cryptography/x509/ocsp.py": 17973800752927680404, + "venv/lib/python3.13/site-packages/numpy/_globals.py": 8736093011628027941, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_salsa.py": 1756536674254931838, + "frontend/src/types/backup.ts": 1741234034896689348, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_categorical.py": 18254320571347851153, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_period.py": 15671388508498441412, + "venv/lib/python3.13/site-packages/pillow.libs/libsharpyuv-95d8a097.so.0.1.2": 5320647379976541240, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/string.tpl": 15796203549130590784, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/licenses/LICENSE": 12684353321716745791, + "venv/lib/python3.13/site-packages/uvicorn/protocols/utils.py": 6147480543268737542, + "frontend/build/_app/immutable/nodes/5.BfJ2ukLY.js": 10444185862953327679, + "frontend/src/routes/tools/debug/+page.svelte": 8699044541980877686, + "backend/src/plugins/search.py": 888817404897740233, + "venv/lib/python3.13/site-packages/pip/_internal/cli/req_command.py": 2887890799766730546, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_timegrouper.py": 276805119790266454, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE": 4123064337800029995, + "venv/lib/python3.13/site-packages/numpy/core/numerictypes.py": 5776941405348611027, + "backend/src/models/__init__.py": 13280325675752905975, + "venv/lib/python3.13/site-packages/PIL/JpegImagePlugin.py": 10621639599916796689, + "venv/lib/python3.13/site-packages/attr/setters.pyi": 6670509359139243236, "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.py": 2490579175967301457, "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_interface.py": 13648006002292914247, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_sql.py": 3901697517087276687, - "venv/lib/python3.13/site-packages/pygments/formatters/terminal.py": 14380579279194052030, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 2991745295485163924, - "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py": 14275839608126658275, - "backend/src/api/routes/git/_deps.py": 12613530531412985767, - "venv/lib/python3.13/site-packages/pip/__pip-runner__.py": 1138979712722199296, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_multiplier.f": 13432803601367981068, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo77.f": 16156304612888138777, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/endpoint.py": 13389763148720370546, - "venv/lib/python3.13/site-packages/h11/_receivebuffer.py": 4631433251046338381, - "venv/lib/python3.13/site-packages/pydantic/v1/datetime_parse.py": 10467114625921924095, - "venv/lib/python3.13/site-packages/pygments/lexers/javascript.py": 16869490851285457857, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_period.py": 6255181868181182780, - "venv/lib/python3.13/site-packages/rsa/cli.py": 1753949198467694190, - "venv/lib/python3.13/site-packages/pydantic/deprecated/class_validators.py": 2200511838255373813, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_impl.py": 12056958946481531279, - "venv/lib/python3.13/site-packages/PIL/IptcImagePlugin.py": 14329453455296600824, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/httpcore/_async/__init__.py": 491192949420297153, - "venv/lib/python3.13/site-packages/_pytest/assertion/__init__.py": 16392348866405910618, - "venv/lib/python3.13/site-packages/urllib3/connection.py": 12761371748350595804, - "venv/lib/python3.13/site-packages/pydantic/v1/fields.py": 3948219773415054006, - "venv/lib/python3.13/site-packages/uvicorn/config.py": 17317823506849880916, - "venv/lib/python3.13/site-packages/pygments/lexers/objective.py": 10966124906622044512, - "venv/lib/python3.13/site-packages/pandas/api/indexers/__init__.py": 17431573013142626187, - "venv/lib/python3.13/site-packages/uvicorn/loops/asyncio.py": 16496916527760969173, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/METADATA": 3188324716322823755, - "venv/lib/python3.13/site-packages/authlib/oidc/registration/__init__.py": 9448552677419908109, - "venv/lib/python3.13/site-packages/pycparser/ply/cpp.py": 1665654017894222868, - "backend/src/services/clean_release/enums.py": 7365670144442292671, - "backend/src/plugins/llm_analysis/plugin.py": 13563674795958736723, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_setops.py": 15365064654930699428, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_overlaps.py": 2328652590379935311, - "venv/lib/python3.13/site-packages/attr/exceptions.py": 11532465891810816417, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_map.py": 1115471561925426082, - "venv/lib/python3.13/site-packages/authlib/oidc/core/util.py": 5364455058429522633, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/RECORD": 7363618870628306370, - "venv/lib/python3.13/site-packages/pygments/lexers/erlang.py": 11757064262773184741, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp.cpython-313-x86_64-linux-gnu.so": 10263790384087091349, - "backend/src/plugins/translate/preview.py": 2758655275240027217, - "frontend/build/_app/immutable/nodes/21.C3rNhw8s.js": 13514883473846080135, - "backend/tests/services/clean_release/test_compliance_task_integration.py": 682622548217611215, - "venv/lib/python3.13/site-packages/cffi/backend_ctypes.py": 2042566833861899211, - "frontend/src/routes/+page.ts": 11160596731849880920, - "backend/tests/test_translate_scheduler_guard.py": 13396608843530344156, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.cpython-313-x86_64-linux-gnu.so": 657138415665704488, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_indexing.py": 11572911419313573163, - "venv/lib/python3.13/site-packages/pygments/lexers/_css_builtins.py": 17478340487186531268, - "venv/bin/pyrsa-verify": 6762739642629904442, - "backend/src/core/middleware/__init__.py": 13917394130915533209, - "venv/lib/python3.13/site-packages/idna/package_data.py": 6583909243002417298, - "venv/lib/python3.13/site-packages/pygments/lexers/smalltalk.py": 6113431347120223592, - "venv/lib/python3.13/site-packages/pygments/lexers/scdoc.py": 8481749542463754279, - "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/backend.py": 9072112660082423691, - "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.py": 14446508217220482719, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/referencing/__init__.py": 10093824711214801917, - "venv/lib/python3.13/site-packages/pandas/core/arrays/string_.py": 14865733321317939478, - "venv/lib/python3.13/site-packages/pandas/core/arraylike.py": 11439602957100017713, - "venv/lib/python3.13/site-packages/pycparser/ply/ctokens.py": 1599952198962458730, - "frontend/build/_app/immutable/chunks/DXB0WcN8.js": 6620985551681767720, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_day.py": 6371851881186970754, - "venv/lib/python3.13/site-packages/pygments/lexers/algebra.py": 9651893228421050246, - "venv/lib/python3.13/site-packages/anyio/abc/_testing.py": 16484863782452886547, - "venv/lib/python3.13/site-packages/pygments/lexers/modeling.py": 15714077241620129689, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_constructors.py": 12628643543371952431, - "venv/lib/python3.13/site-packages/pandas/_libs/indexing.pyi": 90995502095752726, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/idna/codec.py": 4260241549062101779, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_names.py": 10037208029090713121, - "frontend/build/_app/immutable/nodes/14.Dgx5ZI_m.js": 3914755165925651833, - "venv/lib/python3.13/site-packages/gitdb/db/loose.py": 17467413653016810702, - "venv/lib/python3.13/site-packages/pydantic/version.py": 3699343596421001359, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_index_as_string.py": 13421551616136437100, - "venv/lib/python3.13/site-packages/fastapi/security/base.py": 15674297568104917749, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.pyi": 13083543811193933305, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_iloc.py": 16160558158155038350, - "venv/lib/python3.13/site-packages/pygments/lexers/xorg.py": 14591144191879110061, - "backend/src/api/routes/dataset_review_pkg/_dependencies.py": 15939232213193276681, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/styled.py": 8726407298858811635, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_extending.py": 10202091354750153497, - "venv/lib/python3.13/site-packages/PIL/_typing.py": 17407980360703360848, - "venv/lib/python3.13/site-packages/pandas/core/arrays/string_arrow.py": 16997970607998635570, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_numpy_array_equal.py": 2089426719574307767, - "venv/lib/python3.13/site-packages/git/db.py": 6680123567509774609, - "backend/src/api/routes/__tests__/test_clean_release_api.py": 11565478953155085157, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_algorithms.py": 15219408648894515493, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.pyx": 7085380878820747579, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_style.tpl": 3352086677417301397, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/__init__.py": 3969805865048730851, - "venv/lib/python3.13/site-packages/yaml/_yaml.cpython-313-x86_64-linux-gnu.so": 16990140871649227895, - "frontend/src/lib/components/translate/__tests__/test_bulk_replace_modal.svelte.js": 6802604718120565742, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/incfile.f90": 4348588746493647387, - "venv/lib/python3.13/site-packages/pygments/util.py": 4815334059917765852, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_quarter.py": 6795026338946382805, - "venv/lib/python3.13/site-packages/pydantic_settings/main.py": 17272314000918984790, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/precision.f90": 5518911932373565642, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_numpy.py": 9663734591667845940, - "venv/lib/python3.13/site-packages/pygments/lexers/yara.py": 17510792615476493957, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_orm_types.py": 5508682532707462217, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.pyi": 3145980717589050713, - "venv/lib/python3.13/site-packages/pygments/regexopt.py": 16073437303824961930, - "venv/lib/python3.13/site-packages/referencing/tests/test_jsonschema.py": 9815500901482570619, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/default_comparator.py": 47118577003622736, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_time_grouper.py": 3203729024111501071, - "venv/lib/python3.13/site-packages/_pytest/config/compat.py": 13175516717549204627, - "venv/lib/python3.13/site-packages/pygments/lexers/r.py": 14263133575134645005, - "frontend/src/lib/components/translate/TranslationRunProgress.svelte": 16740229145769290247, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/__init__.py": 1437580234680265326, - "venv/lib/python3.13/site-packages/apscheduler/job.py": 1858376671986108667, - "venv/lib/python3.13/site-packages/psycopg2/_json.py": 9729420372294281405, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_core_functionalities.py": 10449747865888948579, - "venv/lib/python3.13/site-packages/h11/_headers.py": 7592730288271984585, - "venv/lib/python3.13/site-packages/gitdb/test/__init__.py": 18393574323937353933, - "venv/lib/python3.13/site-packages/_pytest/logging.py": 10972553724755511468, - "frontend/src/routes/datasets/review/[id]/+page.svelte": 2388350237937943327, - "venv/lib/python3.13/site-packages/h11/_util.py": 9362339074207904329, - "venv/lib/python3.13/site-packages/numpy/_core/_operand_flag_tests.cpython-313-x86_64-linux-gnu.so": 12296711931541109624, - "venv/lib/python3.13/site-packages/PIL/GdImageFile.py": 16875784236394545058, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/parameter.py": 2793853093222900337, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_npfuncs.py": 6132674295193898715, - "frontend/src/lib/api/datasetReview.js": 3023829599261929428, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/core/arrayprint.py": 9652368112174245943, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/base.py": 3628510830969664111, - "venv/lib/python3.13/site-packages/rsa/py.typed": 11041940105507257053, - "venv/lib/python3.13/site-packages/httpx/_exceptions.py": 18399489130669465891, - "backend/src/core/__tests__/test_config_manager_compat.py": 656412918792001580, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_style.py": 10405592363651914796, - "venv/lib/python3.13/site-packages/pygments/lexers/kuin.py": 2818054539831699366, - "venv/lib/python3.13/site-packages/pygments/lexers/ecl.py": 12886695201994337568, - "venv/lib/python3.13/site-packages/pandas/_libs/sas.pyi": 873957906443703977, - "venv/lib/python3.13/site-packages/pydantic/errors.py": 3506829204186508192, - "venv/lib/python3.13/site-packages/pluggy/_result.py": 16790448391766505769, - "venv/lib/python3.13/site-packages/pygments/lexers/dylan.py": 16246299896150112270, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_numba.py": 1548992314517942530, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_constructors.py": 17219764538851313252, - "venv/lib/python3.13/site-packages/pip/_internal/utils/_log.py": 13011536737418204268, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pymysql.py": 12923876736367229454, - "frontend/build/_app/immutable/assets/9.tn0RQdqM.css": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/azure.py": 13529763490186254462, - "venv/lib/python3.13/site-packages/jaraco/functools/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/requests/status_codes.py": 17005213506461710202, - "venv/lib/python3.13/site-packages/pip/_internal/commands/inspect.py": 1401259241736437656, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_validate.py": 2133732492454460539, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/nested_secrets.py": 15691917061715429369, - "venv/lib/python3.13/site-packages/greenlet/greenlet_slp_switch.hpp": 1694714764745718414, - "backend/src/services/dataset_review/semantic_resolver.py": 8447496149691924829, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_mask.py": 9654897363729130542, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/models.py": 17870758731501836303, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_indexing.py": 5063545640498065297, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/auth.py": 13542332344403032059, - "venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py": 10265081145401348863, - "venv/lib/python3.13/site-packages/pandas/core/nanops.py": 18291534943985310005, - "venv/lib/python3.13/site-packages/pygments/styles/vs.py": 4188729978573652106, - "backend/src/api/routes/assistant/_llm_planner.py": 17830859532452456684, - "backend/src/api/routes/git/_environment_routes.py": 1034235394475003092, - "backend/tests/conftest.py": 14115149798000023183, - "venv/lib/python3.13/site-packages/idna/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/replace.py": 1708865864686705833, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_groupby.py": 2738671407653333726, - "venv/lib/python3.13/site-packages/gitdb/test/test_example.py": 3039934065817753241, - "venv/lib/python3.13/site-packages/pyasn1/type/base.py": 6801504886392649787, - "venv/lib/python3.13/site-packages/tzlocal/__init__.py": 11017072814026497364, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.py": 4440659352851541547, - "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.pyi": 17903213294220335051, - "venv/lib/python3.13/site-packages/pydantic/_internal/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unique.py": 6234921572794573905, - "venv/lib/python3.13/site-packages/pandas/_libs/lib.cpython-313-x86_64-linux-gnu.so": 6452315003728497116, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py": 7906029092844029127, - "venv/lib/python3.13/site-packages/pygments/lexers/templates.py": 16267123844830647955, - "frontend/src/lib/components/layout/Sidebar.svelte": 15884718235284801177, - "venv/lib/python3.13/site-packages/pandas/io/html.py": 2727247063231114153, - "frontend/src/routes/settings/+page.ts": 3719008513710037840, - "backend/src/api/routes/__tests__/test_clean_release_v2_api.py": 13464989331176891726, - "venv/lib/python3.13/site-packages/itsdangerous/timed.py": 12571102606719555007, - "venv/lib/python3.13/site-packages/pandas/core/indexes/accessors.py": 14394872772838047912, - "backend/src/services/clean_release/stages/no_external_endpoints.py": 11041035366569218866, - "venv/lib/python3.13/site-packages/pandas/core/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rapidfuzz/utils.pyi": 7400725037332728015, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_loadtxt.py": 17562784940619942447, - "venv/lib/python3.13/site-packages/pygments/styles/stata_dark.py": 324072437809944751, - "backend/src/api/routes/assistant/_dataset_review.py": 14508672919199309579, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_partial.py": 3151286293440416036, - "venv/lib/python3.13/site-packages/pandas/core/strings/object_array.py": 17631047188839635539, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/instrumentation.py": 14882045449667214075, - "venv/lib/python3.13/site-packages/pip/_internal/locations/_sysconfig.py": 4773855595767526182, - "backend/src/plugins/mapper.py": 1970475023222782681, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_linalg.py": 2939739913218718132, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.pyi": 13068936038543812082, - "venv/lib/python3.13/site-packages/pandas/_libs/internals.pyi": 11662356720550765508, - "venv/lib/python3.13/site-packages/pandas/conftest.py": 747626550806435763, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_astype.py": 1961831758405941258, - "venv/lib/python3.13/site-packages/PIL/MpegImagePlugin.py": 7726254767528154514, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_reductions.py": 14432782234851468060, - "backend/src/services/clean_release/stages/data_purity.py": 775548350590498835, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation": 4826455560768665215, - "venv/lib/python3.13/site-packages/websockets/extensions/base.py": 2457233337788783873, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/__init__.py": 8093984834068317399, - "venv/lib/python3.13/site-packages/pandas/core/indexes/timedeltas.py": 13653393430363556843, - "venv/lib/python3.13/site-packages/pandas/util/_print_versions.py": 15326663505771588740, - "venv/lib/python3.13/site-packages/pandas/tests/construction/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/attr/__init__.py": 14436902698244718082, - "venv/lib/python3.13/site-packages/cffi/api.py": 5325821560978502135, - "backend/src/models/llm.py": 14295626272101666880, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pygments/lexers/email.py": 4355246279485460399, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_astype.py": 4023134352880775277, - "venv/lib/python3.13/site-packages/pillow.libs/libxcb-64009ff3.so.1.1.0": 7322566017497612912, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_index_col.py": 17604191644943383567, - "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.h": 5980733638838895780, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/reporters.py": 10760889726586439724, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_unix.h": 13709837360414450184, - "venv/lib/python3.13/site-packages/gitdb/utils/encoding.py": 1668793658272579497, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.py": 4984721349618127021, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_misc.py": 8706265665356094801, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_unittests.py": 18157503533859249155, - "venv/lib/python3.13/site-packages/passlib/handlers/oracle.py": 12959175637015399815, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_join.py": 1697622393195784944, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_value_counts.py": 15633881149834196174, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_split_partition.py": 2715491071550545667, - "venv/lib/python3.13/site-packages/jsonschema_specifications/_core.py": 14812112544772987164, - "venv/lib/python3.13/site-packages/pandas/core/internals/concat.py": 12151675567885981464, - "venv/lib/python3.13/site-packages/pip/_internal/locations/__init__.py": 8147012724727395488, - "venv/lib/python3.13/site-packages/uvicorn/py.typed": 15240312484046875203, - "venv/lib/python3.13/site-packages/numpy/fft/_helper.pyi": 9546918341939102632, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/_spdx.py": 17182188548503720124, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_xs.py": 1897144171324167738, - "venv/lib/python3.13/site-packages/typing_inspection/introspection.py": 13916695705179237047, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_numpy.py": 9825804096508014886, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/stride_tricks.pyi": 5844806555773292985, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_bcrypt.py": 11146468670439284889, - "venv/lib/python3.13/site-packages/more_itertools/recipes.py": 5254043689078173976, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_npfuncs.py": 8238385897366878558, - "venv/lib/python3.13/site-packages/pygments/lexers/freefem.py": 9851243865301700231, - "venv/lib/python3.13/site-packages/pygments/lexers/rdf.py": 14488319292387226505, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py": 6067836157632573182, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asfreq.py": 2701081989348180239, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dtypes.py": 7550022036887594386, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/segment.py": 2555584120510872966, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_interval_array_equal.py": 15654625365223535344, - "backend/src/services/llm_provider.py": 9619956052011769991, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_ops.py": 16346030307660048773, - "venv/lib/python3.13/site-packages/numpy/core/numerictypes.py": 5776941405348611027, - "venv/lib/python3.13/site-packages/pip/_internal/network/lazy_wheel.py": 12936988990566680659, - "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimes.py": 14331984919364185787, - "backend/src/api/routes/__tests__/test_reports_api.py": 17714266401048048508, - "backend/src/core/migration/archive_parser.py": 8828677257278703059, - "venv/lib/python3.13/site-packages/attr/_version_info.pyi": 7141309436445210354, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/WHEEL": 16784970174376303810, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py": 2609365889369107154, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/test_pyinstaller.py": 9430762168079129025, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_type_check.py": 3935091606923106753, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_isocalendar.py": 5704842983090753914, - "venv/lib/python3.13/site-packages/PIL/CurImagePlugin.py": 17697303205012181529, - "venv/lib/python3.13/site-packages/PIL/PcfFontFile.py": 8867692961463798279, - "backend/src/plugins/llm_analysis/models.py": 10099813950073320264, - "venv/lib/python3.13/site-packages/ecdsa/numbertheory.py": 13538480862642502330, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/token_endpoint.py": 6118770923176099889, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iat.py": 3570464615146398178, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_ufunc.py": 669019702416198889, - "venv/lib/python3.13/site-packages/git/objects/tag.py": 7954808535585493300, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_envs.py": 5657151394155576509, - "venv/lib/python3.13/site-packages/charset_normalizer/api.py": 9172704759115759638, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers.py": 4940305787631493030, - "venv/lib/python3.13/site-packages/_pytest/_py/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/io/json/_table_schema.py": 3103776773020058805, - "backend/src/services/clean_release/stages/manifest_consistency.py": 1270636316397468243, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexerrors.py": 4434868847540549873, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/__init__.py": 4781700006554860887, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending.pyx": 756189645898997788, - "venv/lib/python3.13/site-packages/iniconfig/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/handlers/scram.py": 12281469744083867450, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/hist.py": 10928173425078493095, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/errors.py": 16711622442922413520, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_shape_manipulation.pyi": 3533739482029983817, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_hist_box_by.py": 16197297231683144299, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/licenses/LICENSE": 6965457853328313914, + "venv/lib/python3.13/site-packages/numpy/_core/__init__.py": 14951958843029076911, + "venv/lib/python3.13/site-packages/pyasn1/codec/der/__init__.py": 15728752901274520502, "venv/lib/python3.13/site-packages/pandas/io/parsers/c_parser_wrapper.py": 5841064965672872661, - "venv/lib/python3.13/site-packages/websockets/streams.py": 15786581670248099415, - "venv/lib/python3.13/site-packages/certifi/cacert.pem": 14727832973577663826, - "venv/lib/python3.13/site-packages/h11/__init__.py": 222801976520197308, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_constructors.py": 11065058462752831997, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_usecols_basic.py": 16382444980424010514, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/mypy.py": 823265492261272631, - "backend/src/core/task_manager/models.py": 749015551222331342, - "backend/src/api/routes/dashboards/_helpers.py": 16683849709430404975, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/exc.py": 3242780937124999391, - "venv/lib/python3.13/site-packages/pandas/core/apply.py": 17080038932032802984, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reorder_levels.py": 10996225584998355824, - "venv/lib/python3.13/site-packages/pandas/core/arrays/base.py": 664515424699746697, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_quantile.py": 17110581927381599978, - "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.pyi": 1424133074692438241, - "venv/lib/python3.13/site-packages/PIL/MspImagePlugin.py": 3034490726971829439, - "venv/lib/python3.13/site-packages/fastapi/params.py": 14263655860695381481, - "venv/lib/python3.13/site-packages/_pytest/freeze_support.py": 494952714451267295, - "venv/lib/python3.13/site-packages/httpx/_client.py": 1063593889704982038, - "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.pyi": 8916922961301555974, - "venv/lib/python3.13/site-packages/keyring/util/platform_.py": 16978095707950463638, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion": 2607512361823362698, - "venv/lib/python3.13/site-packages/pygments/lexers/fift.py": 7537570342576781911, - "frontend/src/services/storageService.js": 14408290095426695791, - "frontend/src/lib/stores/taskDrawer.js": 6659505033581293370, - "frontend/build/_app/immutable/nodes/10.qO4l_hfq.js": 8054490978078371224, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/RECORD": 10552906886624188222, - "venv/lib/python3.13/site-packages/passlib/win32.py": 5311393450489898748, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_.py": 644667973700652263, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_bool.py": 433342705586552936, - "backend/src/services/resource_service.py": 4718644821742397530, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/LICENSE": 8177430849046795595, - "venv/lib/python3.13/site-packages/numpy/_core/function_base.pyi": 8343632640078378312, - "venv/lib/python3.13/site-packages/pandas/api/extensions/__init__.py": 882327406374020760, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/criterion.py": 2042509334195547606, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_compression.py": 8061288329598682752, - "venv/lib/python3.13/site-packages/pydantic/parse.py": 13750341403702794392, "venv/lib/python3.13/site-packages/numpy/fft/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/apply/common.py": 1113935558495455492, - "venv/lib/python3.13/site-packages/numpy/_core/umath.py": 6132761780113682667, - "venv/lib/python3.13/site-packages/pygments/lexers/parsers.py": 12372594608244797844, - "backend/src/models/clean_release.py": 6080208142954589647, - "venv/lib/python3.13/site-packages/pandas/_testing/_hypothesis.py": 12298650110193262367, - "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/casting.py": 12226803454582253097, - "venv/lib/python3.13/site-packages/pandas/core/computation/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_subclass.py": 5540948578269213810, - "venv/lib/python3.13/site-packages/pandas/core/indexing.py": 15507869313676722071, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/METADATA": 7727489762869155085, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py": 10894229569442891394, - "frontend/build/_app/immutable/chunks/CH5SIC62.js": 11262658403146762189, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_scalar_compat.py": 1306236983392218709, - "backend/src/api/routes/__tests__/test_profile_api.py": 1724401885939803766, - "backend/logs/app.log.3": 366149754645105088, - "venv/lib/python3.13/site-packages/numpy/_array_api_info.pyi": 13283968924732680159, - "dist/docker/postgres.v1.0.0-rc2-docker.tar": 5933711120559789473, - "venv/lib/python3.13/site-packages/pygments/lexers/stata.py": 7249891970174867363, - "backend/src/api/routes/translate/_schedule_routes.py": 18180097500694105300, - "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_version.py": 849232015288488204, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_interval.py": 2613503970235591726, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/connection.py": 5582413226288987808, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_pairwise.py": 2673370322448160842, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/numpy/__init__.py": 2331213407497901634, - "venv/lib/python3.13/site-packages/PIL/ImageMath.py": 7395449734007598797, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_getitem.py": 8414677531950073373, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/__init__.py": 3521833849634327520, + "venv/lib/python3.13/site-packages/pandas/api/indexers/__init__.py": 17431573013142626187, "venv/bin/pip": 13861749540792881808, - "backend/src/core/task_manager/cleanup.py": 8316318318890394605, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/color_triplet.py": 17264298271771184, - "venv/lib/python3.13/site-packages/fastapi/encoders.py": 1914192268055730299, - "venv/lib/python3.13/site-packages/pygments/lexers/installers.py": 5083244110640899732, - "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.pyi": 16176428602589420913, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_quoted_character.py": 2316632434571797367, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi": 1626154870044880426, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/LICENSE": 17675192170670564526, - "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/__init__.py": 8705044848308260480, - "venv/lib/python3.13/site-packages/passlib/tests/__init__.py": 16543763165305017760, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.py": 13853220736499979380, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/implicit.py": 7379110378465568776, - "venv/lib/python3.13/site-packages/numpy/core/_dtype.py": 2806449741844148191, - "venv/lib/python3.13/site-packages/typing_extensions.py": 8402965285733247527, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_with_comments.f": 3980077223827767077, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_cli.py": 3173259257428855556, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/etcd.py": 4078354986605069279, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_indexing.py": 7181087459615188551, - "venv/lib/python3.13/site-packages/fastapi/security/http.py": 8465565118555441408, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_is_monotonic.py": 12899848089975919515, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/processors.py": 4817637175145012571, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/color.py": 15062855556511243610, - "venv/lib/python3.13/site-packages/anyio/_core/_streams.py": 6436461519284884601, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/highlighter.py": 10615901258492570740, - "venv/lib/python3.13/site-packages/_pytest/_io/wcwidth.py": 13515196388826152383, - "venv/lib/python3.13/site-packages/click/exceptions.py": 9734955259447933725, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py": 4633931060151989960, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/WHEEL": 15858869568085970864, - "venv/lib/python3.13/site-packages/uvicorn/main.py": 5843333502356171120, - "venv/lib/python3.13/site-packages/pygments/lexers/verifpal.py": 9190813166686923928, - "venv/lib/python3.13/site-packages/jeepney/auth.py": 10565040329507087892, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/WHEEL": 2357997949040430835, - "frontend/src/lib/components/layout/__tests__/test_sidebar.svelte.js": 1424894894434906249, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_log_render.py": 18076931145915444473, - "venv/lib/python3.13/site-packages/pandas/compat/__init__.py": 6077099417711209303, - "venv/lib/python3.13/site-packages/pandas/io/excel/_odfreader.py": 2760916559416783928, - "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.pyi": 15067467792538416274, - "backend/src/services/rbac_permission_catalog.py": 4811877724682885443, - "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.pyi": 6796322258258182775, - "venv/lib/python3.13/site-packages/passlib/utils/des.py": 2650106381178222229, - "venv/lib/python3.13/site-packages/pandas/core/_numba/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet/PyGreenletUnswitchable.cpp": 3160733517193315684, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arithmetic.py": 3209727509456212551, - "backend/src/api/routes/git/_gitea_routes.py": 14644218433873522624, - "venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py": 7733356903092860495, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/RECORD": 14375680579341422646, - "venv/lib/python3.13/site-packages/numpy/lib/npyio.pyi": 3212440968296555560, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/__init__.py": 1798074863773727377, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_astype.py": 7542068949396266883, - "venv/lib/python3.13/site-packages/websockets/extensions/__init__.py": 13067201313921664210, - "venv/lib/python3.13/site-packages/pandas/core/_numba/extensions.py": 4132402557064663751, - "venv/lib/python3.13/site-packages/cffi/commontypes.py": 11198107299826138465, - "venv/lib/python3.13/site-packages/_pytest/scope.py": 7603155109542903644, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_formats.py": 3525283725314253763, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_almost_equal.py": 9333355453468475330, - "venv/lib/python3.13/site-packages/httpcore/_backends/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/keyring/__main__.py": 14602212239605295451, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/licenses/LICENSE": 11986475601091748012, - "venv/lib/python3.13/site-packages/numpy/_core/defchararray.pyi": 2277933368373498515, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_check_indexer.py": 2929031395357057009, - "backend/src/services/clean_release/stages/base.py": 7739111130895140155, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_compare.py": 171535659467902751, - "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.py": 4037074830103951000, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccosh.csv": 5882025767947638305, - "venv/lib/python3.13/site-packages/pandas/tests/test_register_accessor.py": 16661086720613729974, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_array_from_pyobj.py": 9596224861290975079, - "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector_cpp.cpython-313-x86_64-linux-gnu.so": 16337157355618216738, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth1_client.py": 3129955464287331242, - "venv/lib/python3.13/site-packages/pandas/tests/apply/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_halfyear.py": 587199217232946404, - "venv/lib/python3.13/site-packages/pygments/lexers/jmespath.py": 7426464251945393925, + "frontend/src/lib/components/translate/BulkReplaceModal.svelte": 9447617465987205060, + "venv/lib/python3.13/site-packages/pluggy/_hooks.py": 6941854999025808196, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/WHEEL": 5160964596297665692, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_real.py": 5283415082530228716, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.pyi": 15420142722808118826, + "venv/lib/python3.13/site-packages/numpy.libs/libgfortran-040039e1-0352e75f.so.5.0.0": 13946067868274850557, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot_multilevel.py": 9830258607305392103, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_header.py": 3564391095263545879, + "venv/lib/python3.13/site-packages/pydantic/class_validators.py": 4643597082531287188, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/__init__.py": 1856412417337875350, + "venv/lib/python3.13/site-packages/pygments/lexers/_cocoa_builtins.py": 15468347167912293394, + "frontend/src/components/Toast.svelte": 4155277822937119359, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_version.py": 15336513445621042105, + "venv/lib/python3.13/site-packages/pydantic/v1/__init__.py": 1642038155370734658, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_validate.py": 2133732492454460539, + "venv/lib/python3.13/site-packages/jsonschema/_legacy_keywords.py": 16481666133229715822, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_indexing.py": 9862204223281834360, + "venv/lib/python3.13/site-packages/click/shell_completion.py": 5858110451169927373, + "venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py": 8159971229245441880, + "venv/lib/python3.13/site-packages/PIL/GbrImagePlugin.py": 7681313333602257802, + "venv/lib/python3.13/site-packages/pydantic_settings/__init__.py": 4374954204580208673, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/RECORD": 15574202654585730126, + "venv/lib/python3.13/site-packages/pygments/lexers/dalvik.py": 5549983892200382076, + "frontend/build/_app/immutable/entry/start.1x6iwmQm.js": 11359310873677620769, + "venv/lib/python3.13/site-packages/pydantic/_internal/_validators.py": 2939255306814626438, + "venv/lib/python3.13/site-packages/pygments/lexers/pddl.py": 9256112535059995841, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/comparisons.py": 7327629888601690759, + "venv/lib/python3.13/site-packages/secretstorage/util.py": 1004887530903069622, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_clip.py": 8087139801274517318, + "venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py": 10408538054406276433, + "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.py": 738588649412338185, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/token.py": 6987328478153445584, + "backend/src/plugins/translate/preview_constants.py": 2758310812709243203, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py": 10196543528439925412, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/LICENSE": 2032205056284080825, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api2.pyx": 15936470837420356071, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_numba.py": 6409810497251251179, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine.py": 16193618152415684421, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/__init__.py": 15130871412783076140, + "frontend/src/routes/datasets/review/[id]/__tests__/dataset_review_workspace.ux.test.js": 4105775024258653726, + "venv/lib/python3.13/site-packages/greenlet/tests/test_leaks.py": 1219835696230215605, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_ddl.py": 3690189094298044728, + "venv/lib/python3.13/site-packages/PIL/ImageDraw.py": 12919304671639673744, + "venv/lib/python3.13/site-packages/cffi/model.py": 13544803366359102016, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_function_base.py": 15241999259666702619, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.pyi": 14543653817631274394, + "frontend/build/_app/immutable/nodes/26.8Y2ouxEe.js": 8154056349812602168, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata.py": 2021612995853910808, + "venv/lib/python3.13/site-packages/pandas/core/computation/align.py": 13027916530558901817, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/mod.py": 2054876501774773382, + "venv/lib/python3.13/site-packages/pandas/tests/window/conftest.py": 10673264126684571493, + "venv/lib/python3.13/site-packages/charset_normalizer/md.py": 2029760700380476138, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/checks.pyx": 13187505236986855177, + "venv/lib/python3.13/site-packages/yaml/serializer.py": 1783761365703703558, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi": 13016654355866474644, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_parse_dates.py": 5344929031479212951, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/array.py": 13840510238256626632, + "frontend/src/lib/components/translate/TranslationMetricsDashboard.svelte": 8577281635452165836, "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptodome.py": 15753279711312689193, - "frontend/src/components/tasks/LogFilterBar.svelte": 11464828454938370522, - "backend/src/services/reports/__tests__/test_report_normalizer.py": 5030199452684805352, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/METADATA": 6438256145634479451, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_npfuncs.py": 6132674295193898715, + "frontend/build/_app/immutable/nodes/16.DLQZ3A1Z.js": 5331929660123923733, + "venv/lib/python3.13/site-packages/idna/package_data.py": 6583909243002417298, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_unary.py": 2573825678342936734, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_head_tail.py": 13047338070883375426, + "venv/lib/python3.13/site-packages/numpy/typing/mypy_plugin.py": 1306129775649200113, + "venv/lib/python3.13/site-packages/PIL/_avif.cpython-313-x86_64-linux-gnu.so": 5243449968857258659, + "venv/lib/python3.13/site-packages/rsa/core.py": 13203947234024924497, + "venv/lib/python3.13/site-packages/keyring/backends/Windows.py": 4103830951042463184, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/models.py": 17356033861792227817, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_groupby.py": 12709199432838648700, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_setops.py": 14246225281428284850, + "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators.py": 9928398334052202841, + "backend/src/plugins/storage/__init__.py.bak": 7062955918227442388, + "venv/lib/python3.13/site-packages/pip/_internal/utils/filesystem.py": 7716285033598020316, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/region.py": 12579403230062395563, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_api.py": 1910541672533352625, + "venv/lib/python3.13/site-packages/pygments/styles/solarized.py": 11231143047572616298, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_set_value.py": 4166411740676564820, + "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.pyi": 18395688193449688360, + "backend/src/models/assistant.py": 15004657265198809796, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/padding.py": 7470483648602195268, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/sessions.py": 5400174351499383072, + "venv/lib/python3.13/site-packages/pandas/core/roperator.py": 2587829042468916489, + "venv/lib/python3.13/site-packages/urllib3/_base_connection.py": 14495079097019980154, + "frontend/src/routes/settings/LoggingSettings.svelte": 1737218883899664063, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/registry.py": 13306388361671753373, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_cpu.h": 13799366908726531089, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapper.py": 16715864918034312591, + "venv/lib/python3.13/site-packages/PIL/ImageWin.py": 13635870584232081323, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_hashing.py": 11119284926039008503, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/__init__.py": 17194870661869873083, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_quoted_character.py": 2316632434571797367, + "frontend/src/lib/Counter.svelte": 4002153662176313030, + "frontend/src/routes/profile/__tests__/profile-preferences.integration.test.js": 6561454515118560091, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_setitem.py": 14920902525192671548, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/__init__.py": 13841777369501476946, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlsxwriter.py": 8445547874832015203, + "venv/lib/python3.13/site-packages/pygments/lexers/amdgpu.py": 17249439604111838979, + "backend/src/core/superset_client/_dashboards_list.py": 16574697057051064609, + "backend/src/models/profile.py": 8992580618107123889, + "venv/lib/python3.13/site-packages/pygments/lexers/foxpro.py": 14032656072384708293, + "venv/lib/python3.13/site-packages/ecdsa/_sha3.py": 10353612776869489378, + "venv/lib/python3.13/site-packages/jsonschema/_keywords.py": 949562792260528762, + "venv/lib/python3.13/site-packages/pydantic/deprecated/json.py": 17692639396786690144, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/utils.py": 617314300548081590, + "venv/lib/python3.13/site-packages/jsonschema/_utils.py": 14237077105384631097, + "venv/lib/python3.13/site-packages/numpy/ma/core.py": 338350463523585845, + "venv/lib/python3.13/site-packages/fastapi/middleware/httpsredirect.py": 5775982317459922443, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_math.h": 16701045088211015518, + "venv/lib/python3.13/site-packages/pygments/lexers/_googlesql_builtins.py": 12251289884604745945, + "venv/lib/python3.13/site-packages/git/refs/remote.py": 4391983355222612205, + "frontend/src/routes/settings/__tests__/settings_page.integration.test.js": 8641041148256525929, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_both.f90": 13943066317159236257, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/fallback.py": 4260636031748548345, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/RECORD": 3617343778671172258, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro_py.py": 11991492232381887787, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_missing.py": 6224750552277937853, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ranges.py": 14549386056660231279, + "venv/lib/python3.13/site-packages/ecdsa/keys.py": 10281170640484982154, + "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.pyi": 5269136485429780038, + "venv/lib/python3.13/site-packages/pandas/api/__init__.py": 1145864207387918302, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/licenses/LICENSE": 5669644120528099197, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_finalize.py": 12539815426478689538, + "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.c": 2519219289444051739, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_trio.py": 7459901627473962540, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.py": 1948707545068786, + "venv/lib/python3.13/site-packages/pandas/tests/test_take.py": 17628004130770326677, + "venv/lib/python3.13/site-packages/pygments/lexers/verification.py": 16645885125604874183, + "venv/lib/python3.13/site-packages/anyio/_core/_contextmanagers.py": 1258491775823080427, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_convert_dtypes.py": 13612212066710266457, + "venv/lib/python3.13/site-packages/authlib/oidc/core/claims.py": 2035724920423374302, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_function.py": 13492077564015386528, + "venv/lib/python3.13/site-packages/starlette/config.py": 16509394679506300256, + "venv/lib/python3.13/site-packages/anyio/streams/stapled.py": 9733127303038397237, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/wrapper.py": 14599195010683170123, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets2.py": 11100501798741918610, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_arrow_string_mixins.py": 16862663427780315543, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/events.py": 15099787454570172797, + "frontend/src/components/__tests__/task_log_viewer.test.js": 6512653335179659416, + "backend/src/core/__tests__/test_throttled_scheduler.py": 10949494963069000037, + "backend/src/core/middleware/trace.py": 13093835356377196397, + "venv/lib/python3.13/site-packages/websockets/cli.py": 14049106025886056110, + "venv/lib/python3.13/site-packages/apscheduler/executors/tornado.py": 6635576580306890718, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/missing.py": 11241703105777668183, + "venv/lib/python3.13/site-packages/pycparser/ply/lex.py": 3393278208465358140, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_repeat.py": 16971624301893342868, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/orm.py": 7554417559838785150, + "venv/lib/python3.13/site-packages/referencing/tests/test_core.py": 8280983300775878144, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarmath.py": 9762951701352556447, + "venv/lib/python3.13/site-packages/pydantic/v1/typing.py": 13467331592583800299, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pxd": 13413785818999584986, + "venv/lib/python3.13/site-packages/pillow.libs/libavif-01e67780.so.16.3.0": 13947441788826399574, + "venv/lib/python3.13/site-packages/sqlalchemy/util/compat.py": 16148553252010089561, + "venv/lib/python3.13/site-packages/requests/status_codes.py": 17005213506461710202, + "venv/lib/python3.13/site-packages/sqlalchemy/event/attr.py": 5971663429221923939, + "venv/lib/python3.13/site-packages/numpy/dtypes.pyi": 6713822658171717369, + "backend/src/api/routes/__tests__/test_tasks_logs.py": 18076509977474947273, + "venv/lib/python3.13/site-packages/pyasn1/codec/streaming.py": 13202773554072937140, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/pygments/lexers/testing.py": 17148598391210353576, + "frontend/src/lib/components/translate/TranslationPreview.svelte": 3234775946311369733, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_cat.py": 15988884721768567895, + "backend/src/services/clean_release/repositories/approval_repository.py": 15069637305272665109, + "backend/git_repos/remote/test-repo.git/objects/c5/b992984fe856bd81206304a881faa3087937cf": 2441623644664166468, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/METADATA": 15970460108484113417, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/fetch.py": 13937019222549208796, + "venv/lib/python3.13/site-packages/pydantic/v1/tools.py": 1995883197897112278, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_attr_equal.py": 5612259834202470413, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tz_localize.py": 13699016868476153393, + "venv/lib/python3.13/site-packages/git/objects/__init__.py": 2110615482320355676, + "backend/src/core/task_manager/manager.py": 31861937937351404, + "backend/src/services/__init__.py": 15123879863527585264, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/extensions.py": 217122171232375592, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/numpy.libs/libscipy_openblas64_-096271d3.so": 14338208504131470036, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_constructors.py": 5627819885594450614, + "venv/lib/python3.13/site-packages/passlib/tests/__init__.py": 16543763165305017760, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_indexing.py": 4063662953863959087, + "backend/git_repos/remote/test-repo.git/hooks/pre-merge-commit.sample": 10013699873067956556, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_dtype.py": 12210524152630532747, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/RECORD": 4487477737768669516, "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/recfunctions.py": 18330899924171767482, + "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/test_jsonschema_specifications.py": 12693210357718751894, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi": 1626154870044880426, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_shape_base.py": 4117141349022217163, + "frontend/build/_app/immutable/nodes/27.Ybxz9hPL.js": 15180937521134474651, + "venv/lib/python3.13/site-packages/pyasn1/type/base.py": 6801504886392649787, + "venv/lib/python3.13/site-packages/pygments/lexers/graph.py": 10445044822847205061, + "frontend/src/lib/components/dataset-review/__tests__/us3_execution_batch.ux.test.js": 13416996904332574966, + "venv/lib/python3.13/site-packages/pandas/core/window/common.py": 17956497339875932377, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_groupby.py": 2738671407653333726, + "venv/lib/python3.13/site-packages/pip/_internal/cli/status_codes.py": 9143625681947330756, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_nanfunctions.py": 2356468092336024365, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parsing.py": 2228959015420278458, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_file_handling.py": 732772718210067599, + "venv/lib/python3.13/site-packages/cryptography/x509/base.py": 9124948989225782039, + "venv/lib/python3.13/site-packages/pydantic/annotated_handlers.py": 10186120300163345360, + "venv/lib/python3.13/site-packages/numpy/random/_pickle.py": 7774620933480975313, + "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.py": 8786558894094307341, + "venv/lib/python3.13/site-packages/pandas/tests/extension/json/test_json.py": 12422315190455428620, + "venv/lib/python3.13/site-packages/PIL/AvifImagePlugin.py": 1872412416505003364, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/METADATA": 12747626683405487434, + "backend/src/services/clean_release/candidate_service.py": 8516915994721977806, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/bitgen.h": 12421210767114721499, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/claims.py": 14794441479740527265, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_expanding.py": 9047466276603454007, + "venv/lib/python3.13/site-packages/pandas/tests/config/test_localization.py": 9524481848419616725, + "backend/test.db": 15130871412783076140, + "venv/lib/python3.13/site-packages/starlette/background.py": 10232175205397808417, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_concatenate_chunks.py": 9693420220003544490, + "frontend/src/routes/datasets/review/ReviewWorkspaceHeader.svelte": 15302107925819930527, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/bootstrap.py": 3865947239497500815, + "venv/lib/python3.13/site-packages/numpy/core/records.py": 12604652690721760437, + "venv/lib/python3.13/site-packages/PIL/Image.py": 13615925856053031482, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npy": 9694707264403109829, + "backend/src/plugins/translate/orchestrator_cancel.py": 673611364664367049, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_des.py": 7198931807116651263, + "venv/lib/python3.13/site-packages/urllib3/util/url.py": 15992369651965016538, + "backend/src/models/git.py": 6898938895423263836, + "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.py": 16854377502491291093, + "venv/lib/python3.13/site-packages/keyring/core.py": 2764269590123131634, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_array_utils.py": 14682148205379122638, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/__init__.py": 15130871412783076140, + "frontend/src/routes/settings/MigrationSettings.svelte": 3323392929444734663, + "venv/lib/python3.13/site-packages/referencing/retrieval.py": 11921179805977833120, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/RECORD": 17545477863793260029, + "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.pyi": 9243829615414967057, + "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.py": 17277594393032382485, + "frontend/src/components/git/GitReleasePanel.svelte": 14875953299937841077, + "venv/lib/python3.13/site-packages/attr/validators.pyi": 15579625896285277321, + "venv/lib/python3.13/site-packages/pandas/io/sas/sas_xport.py": 16525568795475918874, + "frontend/src/lib/api/translate/schedules.js": 8616345664085016472, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_equals.py": 18261810467289276844, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_stack_unstack.py": 3737891757817696405, + "frontend/build/_app/immutable/chunks/C9a28aU5.js": 5513483692277369740, + "venv/lib/python3.13/site-packages/jose/backends/native.py": 50023158618000658, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_value_counts.py": 15633881149834196174, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_encoding.py": 6622474614967398246, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/__init__.py": 5863110456909134678, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py": 9627328185578406121, + "venv/lib/python3.13/site-packages/referencing/tests/test_exceptions.py": 10846225989746274213, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp.csv": 601782991752922510, + "venv/lib/python3.13/site-packages/PIL/ImageOps.py": 3884310863548175682, + "venv/lib/python3.13/site-packages/yaml/representer.py": 10550136298978225013, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/METADATA": 2604612822968175379, + "venv/lib/python3.13/site-packages/pyasn1/type/error.py": 1479878696626082697, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_slp_switch.py": 16490710972475154509, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_csky_gcc.h": 12929785267195988209, + "venv/lib/python3.13/site-packages/numpy/tests/test_scripts.py": 5790352482583964348, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/AB.inc": 10441778083831997423, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/common.py": 10996276696597806957, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval.py": 18040371224712437126, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/util.py": 387985965709546512, + "venv/lib/python3.13/site-packages/pydantic/v1/class_validators.py": 6471688474853407718, + "venv/lib/python3.13/site-packages/pip/__main__.py": 574438877257676911, + "venv/lib/python3.13/site-packages/pandas/_libs/index.cpython-313-x86_64-linux-gnu.so": 18409579812171549318, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_exceptions.py": 12145393002202744314, + "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.pyi": 11310761736182213265, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/METADATA": 16700674024421191168, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_generic.py": 1297407268239966429, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml_dtypes.py": 18293138335799308439, + "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.pyi": 15848834171197104301, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_julian_date.py": 850085757774959003, + "venv/lib/python3.13/site-packages/gitdb/db/loose.py": 17467413653016810702, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py": 1129954411080617001, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.f": 9330535808093475892, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_indexing.py": 1947969642791009285, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_unique.py": 3989602816674881215, + "venv/lib/python3.13/site-packages/pandas/io/orc.py": 12754290195886557668, + "venv/lib/python3.13/site-packages/_pytest/python_api.py": 12953851690889336092, + "venv/lib/python3.13/site-packages/sqlalchemy/util/typing.py": 17638587764345605306, + "venv/lib/python3.13/site-packages/PIL/QoiImagePlugin.py": 17663704273100331654, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/twisted.py": 16059862619165885730, + "venv/lib/python3.13/site-packages/httpcore/_models.py": 2501866419898326733, + "venv/lib/python3.13/site-packages/ecdsa/curves.py": 812969717084691115, + "venv/lib/python3.13/site-packages/cffi/cffi_opcode.py": 8411344362428947268, + "venv/lib/python3.13/site-packages/_pytest/monkeypatch.py": 2635774457434545573, + "venv/lib/python3.13/site-packages/pydantic/_internal/_core_utils.py": 13248004771466837645, + "venv/lib/python3.13/site-packages/pygments/lexers/urbi.py": 1611762607688882372, + "venv/lib/python3.13/site-packages/pygments/lexers/basic.py": 12723021007042054402, + "venv/lib/python3.13/site-packages/pygments/lexers/esoteric.py": 16839499623020685146, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/INSTALLER": 17282701611721059870, + "frontend/src/routes/dashboards/health/__tests__/health_page.integration.test.js": 11653729504599199718, + "backend/src/core/superset_client/_dashboards_crud.py": 4664728216214173491, + "venv/lib/python3.13/site-packages/requests/structures.py": 2668010839316715865, + "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.py": 15854257948148516490, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_results.py": 18145930285449734975, + "venv/lib/python3.13/site-packages/pygments/styles/paraiso_light.py": 15941591873059208999, + "venv/lib/python3.13/site-packages/pandas/_libs/writers.cpython-313-x86_64-linux-gnu.so": 6878855127769203372, + "venv/lib/python3.13/site-packages/pygments/styles/stata_dark.py": 324072437809944751, + "venv/lib/python3.13/site-packages/pycparser/c_ast.py": 5942107079119742, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py": 7573043358244876807, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_frozen.py": 3484963987053884566, + "venv/lib/python3.13/site-packages/pygments/lexers/pony.py": 2720635393743617141, + "venv/lib/python3.13/site-packages/dotenv/parser.py": 17651523026132242005, + "venv/lib/python3.13/site-packages/numpy/_core/getlimits.pyi": 15746478550640231553, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/cmac.py": 12011944740750051829, + "venv/lib/python3.13/site-packages/numpy/_core/lib/pkgconfig/numpy.pc": 14713682399742251335, + "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.pyi": 6546751036506752550, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/gh18403_mod.f90": 6219951399575860362, + "venv/lib/python3.13/site-packages/PIL/PcfFontFile.py": 8867692961463798279, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_with_comments.f": 3980077223827767077, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pygments/lexers/_openedge_builtins.py": 4979988747924637739, + "venv/lib/python3.13/site-packages/pygments/lexers/pawn.py": 18127303119414402594, + "venv/lib/python3.13/site-packages/PIL/Hdf5StubImagePlugin.py": 15654153796225833950, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/REQUESTED": 15130871412783076140, + "backend/src/core/auth/__init__.py": 16435384828055133073, + "venv/lib/python3.13/site-packages/numpy/lib/npyio.py": 6782361985454523908, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/challenge.py": 6388445104761482746, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/jupyter.py": 4446535289156468239, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/palette.py": 17383863392192572184, + "venv/lib/python3.13/site-packages/anyio/_backends/_trio.py": 4764555436057754985, + "backend/src/services/clean_release/exceptions.py": 14631509303697939633, + "backend/src/services/security_badge_service.py": 6269749758059921511, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/format": 12697928260201325593, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/base.py": 3250660921154829815, + "venv/lib/python3.13/site-packages/uvicorn/middleware/__init__.py": 15130871412783076140, + "backend/src/core/database.py.bak": 14909628735913201532, + "venv/lib/python3.13/site-packages/pluggy/__init__.py": 8269188363563356631, + "venv/lib/python3.13/site-packages/pygments/lexers/_tsql_builtins.py": 829193091321611630, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/infer.py": 12541904372617915365, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_concat.py": 10207975532842927134, + "venv/lib/python3.13/site-packages/httpx/_transports/base.py": 6498990531443644896, + "venv/lib/python3.13/site-packages/cryptography/utils.py": 18088508841978595788, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/__init__.py": 13584649922093198724, + "backend/src/api/routes/connections.py": 6022659034509664689, + "venv/lib/python3.13/site-packages/pygments/lexers/freefem.py": 9851243865301700231, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/jwe.py": 10478268710546063196, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/INSTALLER": 17282701611721059870, + "frontend/src/components/tools/MapperTool.svelte": 5334953422209152843, + "backend/src/services/__tests__/test_llm_provider.py": 4705572506930455768, + "backend/src/plugins/llm_analysis/models.py": 10099813950073320264, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/multiarray.pyi": 17232959648651278910, + "venv/lib/python3.13/site-packages/pandas/core/methods/selectn.py": 13552176539021625776, + "venv/lib/python3.13/site-packages/pydantic/_internal/_validate_call.py": 4078236486720616395, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py": 14454690938977069049, + "backend/src/api/routes/__tests__/conftest.py": 8465980330386544642, + "venv/lib/python3.13/site-packages/pandas/core/internals/api.py": 14632030170441237968, + "venv/lib/python3.13/site-packages/greenlet/greenlet_msvc_compat.hpp": 12504008513945919250, + "venv/lib/python3.13/site-packages/smmap/util.py": 13276170835044840023, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_frame.py": 9378623452601244234, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.py": 4265012785873472600, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_datetime.py": 13781712980921881470, + "venv/lib/python3.13/site-packages/sqlalchemy/__init__.py": 9751988289698484530, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/INSTALLER": 17282701611721059870, + "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py": 17792688368296989200, + "backend/src/plugins/translate/preview_session_ops.py": 14499196532448992687, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_windows.py": 9171347078900057357, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_timezones.py": 15157687716502574038, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/core": 10616484054735611337, + "venv/lib/python3.13/site-packages/_pytest/scope.py": 7603155109542903644, + "venv/lib/python3.13/site-packages/httpcore/_async/socks_proxy.py": 9026788710898138090, + "venv/lib/python3.13/site-packages/pygments/lexers/apdlexer.py": 14222752852221009085, + "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.cpython-313-x86_64-linux-gnu.so": 16963890771704942465, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/PdfParser.py": 13710342183037972042, + "venv/lib/python3.13/site-packages/pygments/lexers/usd.py": 934820244673741695, + "venv/lib/python3.13/site-packages/pygments/lexers/_sourcemod_builtins.py": 11809503654078535014, + "venv/lib/python3.13/site-packages/passlib/tests/utils.py": 15403198680231294263, + "venv/lib/python3.13/site-packages/fastapi/_compat/__init__.py": 12380131786677943349, + "venv/lib/python3.13/site-packages/click/_textwrap.py": 14586282603215052310, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/exceptions.py": 14982764273485726199, + "venv/lib/python3.13/site-packages/pydantic/__init__.py": 11536052293866174672, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/__init__.py": 635200170992032966, + "venv/lib/python3.13/site-packages/attr/filters.py": 14156141488477438777, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fromnumeric.pyi": 8671744104085911610, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_masked_matrix.py": 1444179242134203344, + "venv/lib/python3.13/site-packages/numpy/fft/__init__.py": 9343449448737026658, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_builtin_md4.py": 16840174243259839994, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_constructors.py": 9767900500541600486, + "venv/lib/python3.13/site-packages/cffi/__init__.py": 13427266446746498548, + "venv/lib/python3.13/site-packages/pandas/arrays/__init__.py": 10005095663840461954, + "frontend/src/lib/components/reports/__tests__/report_type_profiles.test.js": 498330292141991614, + "venv/lib/python3.13/site-packages/passlib/registry.py": 5050675058011850564, + "frontend/src/lib/components/layout/Sidebar.svelte": 8588237337148619706, + "backend/src/core/migration/dry_run_orchestrator.py": 8630137056901487805, + "backend/git_repos/remote/test-repo.git/hooks/applypatch-msg.sample": 12627299623252391515, + "venv/lib/python3.13/site-packages/websockets/legacy/client.py": 4032878587330592478, + "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/__init__.py": 4290669519959060720, + "venv/lib/python3.13/site-packages/pydantic/dataclasses.py": 12384956801113860889, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_modules.py": 16835987029123241300, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype.pyi": 17662618580381221543, + "venv/bin/f2py": 11006035631504708368, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_integer.py": 16102640589300356639, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asfreq.py": 2701081989348180239, + "frontend/src/lib/components/reports/ReportCard.svelte": 13714680961668469204, + "venv/lib/python3.13/site-packages/passlib/ext/__init__.py": 15240312484046875203, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarinherit.py": 15240535501243623986, + "venv/lib/python3.13/site-packages/pydantic/_internal/_internal_dataclass.py": 13206778611516171760, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_quarter.py": 11099677280196239755, + "venv/lib/python3.13/site-packages/pandas/core/_numba/executor.py": 2205130934861422769, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_setops.py": 9991233160569430204, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_version.py": 10384171076976071398, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_series_equal.py": 1223890028205011021, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_nat.py": 18428782698385974380, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_sort.py": 11941630642630249576, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_at.py": 1029744344133153888, + "venv/lib/python3.13/site-packages/pygments/lexers/asn1.py": 15030988816882821134, + "venv/lib/python3.13/site-packages/pandas/errors/cow.py": 14381282473643980776, + "frontend/src/lib/components/reports/__tests__/fixtures/reports.fixtures.js": 10647302793388244094, + "venv/lib/python3.13/site-packages/pandas/core/window/numba_.py": 1444123360986509769, + "venv/lib/python3.13/site-packages/git/objects/submodule/util.py": 8621897576747443758, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_ufunc.py": 669019702416198889, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/subcomponents.py": 10365594934527289233, + "venv/lib/python3.13/site-packages/charset_normalizer/version.py": 9948878594498587774, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_manylinux.py": 12272275001961360101, + "frontend/build/_app/immutable/chunks/CbSNjLmH.js": 12377182002440958587, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/operators.f90": 5949265961908386974, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/RECORD": 1689342214025776630, + "venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py": 7899068668225389287, + "venv/lib/python3.13/site-packages/pyasn1/type/constraint.py": 16776615503418773283, + "venv/lib/python3.13/site-packages/python_multipart/decoders.py": 14252672585219598429, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategy_options.py": 16342048970907383117, + "backend/src/plugins/__init__.py": 17194112070692432889, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_character.py": 17168707712220699738, + "backend/alembic/versions/b1c2d3e4f5a6_add_source_hash_to_translation_records.py": 4775534557742900399, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.asm": 15349921092257597740, + "venv/lib/python3.13/site-packages/greenlet/TGreenlet.hpp": 6993553444981671786, + "venv/lib/python3.13/site-packages/authlib/oidc/discovery/__init__.py": 308178910015974550, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_searchsorted.py": 11208047646304753715, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data": 11957232619107868023, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/request.py": 10492090878715126550, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_pick.py": 15760492005471315814, + "venv/lib/python3.13/site-packages/numpy/polynomial/_polytypes.pyi": 7528777458251688517, + "venv/lib/python3.13/site-packages/numpy/lib/scimath.py": 7616377937400918724, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_like.pyi": 1968570015765106620, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarprint.py": 9482100952562512933, + "venv/lib/python3.13/site-packages/passlib/handlers/digests.py": 18158237109145814857, + "venv/lib/python3.13/site-packages/passlib/handlers/__init__.py": 3424087230285514388, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/__init__.py": 18036348770966744795, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/data.f90": 13833561828029901615, + "frontend/src/lib/stores.js": 8062876368729352764, + "backend/src/plugins/translate/__tests__/test_scheduler.py": 4988970087981965515, + "backend/git_repos/remote/test-repo.git/objects/4e/227f4d8289ea657ae813b1460f979ae4b676b3": 5659284628452785331, + "venv/bin/dotenv": 953953349803507198, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py": 9673372914396625461, + "venv/lib/python3.13/site-packages/smmap/test/lib.py": 2924931247676024085, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/converter.py": 9041278869295701566, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isin.py": 8136794647591341784, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_numeric.py": 18037815278545274402, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_rowcount.py": 6682800584070485197, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/models.py": 15928924478695638423, + "venv/lib/python3.13/site-packages/urllib3/_request_methods.py": 13671055114247516883, + "venv/lib/python3.13/site-packages/uvicorn/middleware/message_logger.py": 14825516604555033580, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_deprecations.py": 16367195310156359251, + "venv/lib/python3.13/site-packages/pandas/io/common.py": 12088195203620154171, + "backend/src/core/utils/superset_compilation_adapter.py": 4974836712746468095, + "backend/src/models/__tests__/test_clean_release.py": 3448406855108346883, + "backend/git_repos/remote/test-repo.git/HEAD": 12394318939628232491, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dtype.py": 5553691086633215767, + "venv/lib/python3.13/site-packages/pygments/lexers/nix.py": 15050086330545600356, + "venv/lib/python3.13/site-packages/pygments/lexers/pointless.py": 17552020039609007881, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_boxplot_method.py": 7635131059308651529, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_str.py": 149688376805129532, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_quantile.py": 11273403743157110261, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_style.py": 10405592363651914796, + "backend/tests/test_models.py": 3998442321184404421, + "venv/lib/python3.13/site-packages/pluggy/_result.py": 16790448391766505769, + "venv/lib/python3.13/site-packages/pandas/_libs/ops.pyi": 1192312467209548763, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_repeat.py": 9155935990114743583, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_like.py": 2778905818573625118, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_convert.py": 77283525137564255, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_np_datetime.py": 18363183516409621205, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/associationproxy.py": 7421910137064160936, + "venv/lib/python3.13/site-packages/pip/_internal/commands/list.py": 13035925292530461722, + "frontend/src/routes/+error.svelte": 9437886843842311915, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py": 13869355993837330406, + "venv/lib/python3.13/site-packages/cffi/lock.py": 13507686939203117974, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/mypy.ini": 4763385838822685881, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_stata.py": 3030675968330529905, + "venv/lib/python3.13/site-packages/pandas/io/formats/string.py": 14718123345899686696, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/__init__.py": 7526309124863950484, + "venv/lib/python3.13/site-packages/numpy/lib/array_utils.pyi": 14646405559193539455, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/path_registry.py": 8410829659513707067, + "frontend/src/routes/settings/automation/+page.svelte": 17397435134589275932, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/style.py": 12654125988995362593, + "venv/lib/python3.13/site-packages/pandas/core/series.py": 11618272682977710696, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/METADATA": 15987199384245264478, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_index_new.py": 796607958391882182, + "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.py": 17192211156810267818, + "venv/lib/python3.13/site-packages/pandas/core/indexes/accessors.py": 14394872772838047912, + "venv/lib/python3.13/site-packages/fastapi/param_functions.py": 14585101566697773072, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isin.py": 14387850595546682314, + "backend/src/core/task_manager/lifecycle.py": 7861582737135324044, + "venv/lib/python3.13/site-packages/pandas/core/sparse/api.py": 9094651265692218821, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_limited_api.py": 15788485008764560845, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/cast.py": 342228968652072052, + "venv/lib/python3.13/site-packages/pycparser/_ast_gen.py": 5464347629908906775, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_multiindex.py": 13723677252432338937, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/__init__.py": 6327819603201859020, + "venv/lib/python3.13/site-packages/pydantic_settings/exceptions.py": 18300876747412267999, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_npfuncs.py": 8238385897366878558, + "venv/lib/python3.13/site-packages/pandas/core/arrays/numeric.py": 17834058423729918687, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_reindex.py": 15794750188594767026, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_common.py": 9446944526144164255, + "venv/lib/python3.13/site-packages/rapidfuzz/process_py.py": 17563673072076876125, + "venv/lib/python3.13/site-packages/pandas/_libs/lib.pyi": 3049004444338566197, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_round.py": 8336918400205461173, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py": 12632271159842988561, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/METADATA": 6596896092754256988, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/__init__.py": 15130871412783076140, + "backend/src/core/utils/matching.py": 12567367681677699487, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_msvc.h": 17581467060937454503, + "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_config.py": 3636377988901907424, + "venv/lib/python3.13/site-packages/pygments/lexers/forth.py": 17623561228839319663, + "venv/lib/python3.13/site-packages/PIL/ImageFont.py": 15621336334706847865, + "frontend/src/routes/storage/+page.svelte": 5003283673039328108, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop_duplicates.py": 2397396876872403048, + "venv/lib/python3.13/site-packages/apscheduler/triggers/base.py": 1170652622104554892, + "frontend/src/components/tasks/LogEntryRow.svelte": 3694709927263161265, + "venv/lib/python3.13/site-packages/h11/_connection.py": 6971907312737339559, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarraytypes.h": 6765088137325865789, + "frontend/build/_app/immutable/nodes/28.DY1VxtMo.js": 10916149061941853879, + "venv/lib/python3.13/site-packages/pydantic/tools.py": 8931184638883701008, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_transform.py": 6759417099641034692, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/util.py": 955993052529739164, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/dynamic.py": 15220116196072524330, + "backend/src/core/task_manager/__tests__/test_context.py": 4211690952139950648, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_normalize.py": 7378656958631172603, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/strings.pyi": 15361498560917514616, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/METADATA": 13776251749114562660, + "venv/lib/python3.13/site-packages/pip/_internal/utils/egg_link.py": 18211947508793805393, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_size.py": 12449835577388069407, + "venv/lib/python3.13/site-packages/pygments/lexers/business.py": 10410439210806697214, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/scalars.pyi": 17683675356659300769, + "venv/lib/python3.13/site-packages/jeepney/io/common.py": 15160647397460834508, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_reductions.py": 633644255158859565, + "venv/lib/python3.13/site-packages/pygments/lexers/c_cpp.py": 1211842877043321095, + "venv/lib/python3.13/site-packages/dotenv/cli.py": 7952654621986067973, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/json_schema_test_suite.py": 6152158310190822762, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libldap-2974a1ba.so.2.0.200": 13590569080612602445, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/RECORD": 2963644632913976754, + "venv/lib/python3.13/site-packages/_pytest/hookspec.py": 387790426108810884, + "venv/lib/python3.13/site-packages/pandas/tseries/api.py": 14785127136180503704, + "venv/lib/python3.13/site-packages/pygments/styles/colorful.py": 15708848112363953394, + "venv/lib/python3.13/site-packages/pygments/styles/fruity.py": 7925019839136894457, + "venv/lib/python3.13/site-packages/pygments/lexers/ruby.py": 6898093948224302592, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py": 6678500392137188521, + "backend/src/services/notifications/__init__.py": 13930898146447654299, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 2991745295485163924, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarbuffer.py": 16859789411010045795, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_encs.py": 9703572751543253904, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/METADATA": 10220680536734745103, "venv/lib/python3.13/site-packages/pip/_vendor/rich/_extension.py": 4831602527247293866, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_drop.py": 11341278355685583133, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_round.py": 14996007456708990295, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/common.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/pyodbc.py": 4146960201984489604, + "venv/lib/python3.13/site-packages/PIL/_util.py": 17116054813914373606, + "venv/lib/python3.13/site-packages/pyasn1/debug.py": 6305633719847955310, + "venv/lib/python3.13/site-packages/numpy/_core/function_base.pyi": 8343632640078378312, + "venv/lib/python3.13/site-packages/pandas/core/tools/times.py": 6560679305255812809, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_elements_constructors.py": 9279447066034364952, + "frontend/src/lib/components/dataset-review/ExecutionMappingReview.svelte": 6341989866442424314, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.cpython-313-x86_64-linux-gnu.so": 16927263232848238698, + "venv/lib/python3.13/site-packages/pydantic/_internal/_known_annotated_metadata.py": 9308620143218174987, + "backend/src/plugins/translate/service_inline_correction.py": 11066393548140180037, + "backend/alembic.ini": 8897071018956157084, + "venv/lib/python3.13/site-packages/greenlet/PyGreenletUnswitchable.cpp": 3160733517193315684, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/__init__.py": 913610541217778747, + "venv/lib/python3.13/site-packages/pandas/compat/__init__.py": 6077099417711209303, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/unicode_comment.f90": 11326797716215117266, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_cumulative.py": 6346947559103505068, + "venv/lib/python3.13/site-packages/pygments/lexers/ezhil.py": 10294520768762308887, + "backend/src/services/clean_release/__tests__/test_preparation_service.py": 4958420988131940633, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/rule.py": 5318344538761246105, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/METADATA": 10810011740984652381, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_codes.py": 13901854719435716195, + "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.py": 14034239842770171660, + "venv/lib/python3.13/site-packages/passlib/apache.py": 2357244937629445006, + "venv/lib/python3.13/site-packages/passlib/crypto/des.py": 14699826058580193499, + "venv/lib/python3.13/site-packages/pandas/core/arrays/masked.py": 13977195666947626206, + "venv/lib/python3.13/site-packages/pydantic/deprecated/decorator.py": 8609500261161560793, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets.py": 4499538899492148499, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_openid.py": 8708980645667847439, + "venv/lib/python3.13/site-packages/pandas/io/__init__.py": 9767690163638898709, + "frontend/build/_app/immutable/chunks/2-sJyMFl.js": 17823400295919968278, + "backend/tests/core/test_mapping_service.py": 6888870177029085220, + "backend/git_repos/remote/test-repo.git/objects/93/9cc583a18267406014c57617ccb557569f6aaf": 5474754694741999746, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_printing.py": 1901396869745313303, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_utils.pyi": 3708905060001289515, + "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.cpp": 7360124705400993707, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_mips_unix.h": 17373389962405490482, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/util.py": 2866856507760047219, + "venv/lib/python3.13/site-packages/_pytest/config/compat.py": 13175516717549204627, + "venv/lib/python3.13/site-packages/pydantic/fields.py": 4142211199816898047, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.py": 1341849861184131135, + "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending.py": 16515485226251040454, + "venv/lib/python3.13/site-packages/_pytest/pytester_assertions.py": 1899505062406979663, + "venv/lib/python3.13/site-packages/fastapi/security/open_id_connect_url.py": 3502444317759870079, + "venv/lib/python3.13/site-packages/numpy/_core/printoptions.pyi": 16758392990920041462, + "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.py": 11393954913234866059, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath.py": 13010939919217862164, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_datetimelike.py": 15246580603900811749, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_accessor.py": 10074496835594855786, + "venv/lib/python3.13/site-packages/authlib/integrations/django_client/apps.py": 14151733532064278336, + "venv/lib/python3.13/site-packages/numpy/_typing/_dtype_like.py": 1598511035344858524, + "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.py": 4984721349618127021, + "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.py": 12886275429547303345, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_categorical.py": 10863578845754547294, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/setup.py": 17086801293303202021, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/__init__.py": 5118993772178969104, + "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.pyi": 16799580742094025110, + "venv/lib/python3.13/site-packages/httpcore/_backends/sync.py": 576247220834179928, + "venv/lib/python3.13/site-packages/pygments/styles/zenburn.py": 1031869867933871309, + "venv/lib/python3.13/site-packages/numpy/f2py/__init__.pyi": 3566887042414273892, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_comparison.py": 4611207880323503098, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/asyncio.py": 2517623574187179319, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_xport.py": 14080914637071212225, + "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/okp_key.py": 10312614562914096039, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/comal.py": 769727871242464458, + "frontend/src/lib/components/translate/TermCorrectionPopup.svelte": 5276611147503580165, + "frontend/build/_app/immutable/chunks/DSVdJG2-.js": 9023826012197734009, + "venv/lib/python3.13/site-packages/numpy/tests/test_warnings.py": 3154249157134620649, + "venv/lib/python3.13/site-packages/pandas/_testing/_warnings.py": 5640964566228306285, + "backend/src/services/__tests__/test_rbac_permission_catalog.py": 5190751380981933250, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh17797.f90": 17319074662077763768, + "venv/lib/python3.13/site-packages/PIL/ImageDraw2.py": 13610162107593278891, + "venv/lib/python3.13/site-packages/pip/_internal/utils/compatibility_tags.py": 15862649917149158291, + "venv/lib/python3.13/site-packages/pytest/__main__.py": 8747175113746141224, + "venv/lib/python3.13/site-packages/_pytest/_py/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/_imagingft.cpython-313-x86_64-linux-gnu.so": 11054435406031360190, + "docker/backend.entrypoint.sh": 3113473166911458895, + "venv/lib/python3.13/site-packages/pip/_internal/commands/hash.py": 11824898770873127699, + "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.pyi": 9911899303567570646, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_astype.py": 563689238486630866, + "frontend/src/routes/settings/notifications/+page.svelte": 14504217413720856827, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_core.py": 12712110303898046818, + "venv/lib/python3.13/site-packages/_pytest/doctest.py": 6126919596776592278, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_item_selection.py": 1027989019709029615, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/METADATA": 7214286447912789067, + "backend/src/services/git/_url.py": 15729080796864187707, + "backend/src/plugins/llm_analysis/__tests__/test_service.py": 9850768293526486431, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/version.py": 6401479036177132374, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.py": 5933622200264572885, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas7bdat.py": 8963983384208121526, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_other.py": 783701524491896337, + "venv/lib/python3.13/site-packages/pygments/lexers/dotnet.py": 17970884907837213099, + "venv/lib/python3.13/site-packages/pygments/lexers/gsql.py": 15294109558028161389, + "venv/lib/python3.13/site-packages/pandas/io/formats/style.py": 13235403689320479165, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_partial_slicing.py": 15567512240469921599, + "venv/lib/python3.13/site-packages/pygments/lexers/scdoc.py": 8481749542463754279, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/transforms.py": 3773159138960141434, + "venv/lib/python3.13/site-packages/greenlet/platform/setup_switch_x64_masm.cmd": 8473866644286785815, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_indexing.py": 13703573544185051910, + "venv/lib/python3.13/site-packages/pygments/lexers/tls.py": 3552078393009383412, + "venv/lib/python3.13/site-packages/urllib3/http2/__init__.py": 62108352209996836, + "backend/src/api/routes/datasets.py": 8495307661445562003, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/__init__.py": 5027355874704300832, + "venv/lib/python3.13/site-packages/numpy/_core/_rational_tests.cpython-313-x86_64-linux-gnu.so": 13475521165331256711, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_setops.py": 15196593522537569500, + "frontend/src/components/RepositoryDashboardGrid.svelte": 14921738329941705036, + "venv/lib/python3.13/site-packages/fastapi/security/utils.py": 2616633862993539558, + "venv/lib/python3.13/site-packages/yaml/emitter.py": 6036657460416912844, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.py": 4672871351134074735, + "venv/lib/python3.13/site-packages/pip/_internal/pyproject.py": 6155748929233245415, + "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/algorithms.py": 2375452080667815941, + "venv/lib/python3.13/site-packages/PIL/ImageStat.py": 18442709639217312689, + "frontend/src/components/git/__tests__/git_manager.unfinished_merge.integration.test.js": 4508809242640248036, + "venv/lib/python3.13/site-packages/passlib/handlers/scrypt.py": 10089372287597257402, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_shares_memory.py": 2911029123657970431, + "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.py": 6142062990691556076, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_equals.py": 9903250467264848187, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/ansi.py": 948249091929588017, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/resource_protector.py": 3563910821279222695, + "venv/lib/python3.13/site-packages/jose/backends/_asn1.py": 14123487576864105793, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_misc.pyi": 7544619194032232861, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/util.py": 1520899420719605380, + "venv/lib/python3.13/site-packages/pygments/formatters/bbcode.py": 8614391937525677922, + "venv/lib/python3.13/site-packages/pygments/lexers/smv.py": 8253688483040957894, + "frontend/build/_app/immutable/chunks/J6m7GrH4.js": 13522826261624448243, + "venv/lib/python3.13/site-packages/authlib/oidc/discovery/models.py": 6924198551176217533, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate.py": 16723865897349422996, + "venv/lib/python3.13/site-packages/jose/backends/ecdsa_backend.py": 2684176977865283060, + "venv/lib/python3.13/site-packages/_pytest/_version.py": 4890545974654141536, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py": 8412594868333244844, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_tokenizer.py": 7994281991455916540, + "venv/lib/python3.13/site-packages/numpy/random/_pickle.pyi": 16081120364372391381, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_reduction.py": 2671872764983553906, + "run_clean_tui.sh": 14305120550349644601, + "venv/lib/python3.13/site-packages/greenlet/tests/__init__.py": 1181423074160870921, + "frontend/src/components/Navbar.svelte": 10594730954703520477, + "venv/lib/python3.13/site-packages/pandas/_libs/hashing.pyi": 5241441378073929968, + "backend/src/api/routes/assistant/_history.py": 16318881016647964852, + "venv/lib/python3.13/site-packages/jsonschema/exceptions.py": 15651767069704734674, + "venv/lib/python3.13/site-packages/dateutil/tz/_common.py": 9009072600471223066, + "venv/lib/python3.13/site-packages/pygments/lexers/verifpal.py": 9190813166686923928, + "venv/lib/python3.13/site-packages/pygments/lexers/rdf.py": 14488319292387226505, + "venv/lib/python3.13/site-packages/pydantic/v1/_hypothesis_plugin.py": 6383299780629629080, + "venv/lib/python3.13/site-packages/PIL/MpoImagePlugin.py": 644327650732339301, + "frontend/src/lib/auth/permissions.js": 12787489292887903923, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated": 11193150813314469394, + "backend/git_repos/remote/test-repo.git/objects/c2/491dfd8c841bacd5f491510f6334aa4a626f00": 6297303620364618482, + "backend/git_repos/remote/test-repo.git/objects/d8/3fe0db1279ef07c4c4f3de4156a629373b4057": 7507193819578792576, + "venv/lib/python3.13/site-packages/urllib3/_collections.py": 3440910792774742262, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_integrity.py": 12010564145080323987, + "frontend/e2e/tests/settings.e2e.js": 14741334477359695649, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_sort_values.py": 15072305514178699398, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccos.csv": 3026156288992132911, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_constructors.py": 12118296571694122743, + "venv/lib/python3.13/site-packages/jsonschema/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/LICENSE": 17675192170670564526, + "frontend/src/routes/settings/+page.ts": 3719008513710037840, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py": 2516476609539242190, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp.cpython-313-x86_64-linux-gnu.so": 10263790384087091349, + "venv/lib/python3.13/site-packages/pandas/_libs/groupby.pyi": 6154952803757623626, + "venv/bin/activate.csh": 14513616043256836238, + "frontend/src/routes/translate/dictionaries/+page.svelte": 1038433841421198720, + "venv/lib/python3.13/site-packages/rapidfuzz/process.py": 3870006735299537016, + "venv/lib/python3.13/site-packages/pip/_internal/models/link.py": 7172346414535657925, + "venv/lib/python3.13/site-packages/pandas/core/window/online.py": 13551140636252302201, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_get_numeric_data.py": 11747933921770969706, + "venv/lib/python3.13/site-packages/pandas/_testing/asserters.py": 272063856832951430, + "venv/lib/python3.13/site-packages/starlette/concurrency.py": 9835859079437525910, + "venv/lib/python3.13/site-packages/urllib3/util/util.py": 12490259045660543460, + "backend/src/api/routes/__tests__/test_clean_release_source_policy.py": 15242462571646635922, + "backend/tests/scripts/test_clean_release_tui.py": 14700450036639944764, + "venv/lib/python3.13/site-packages/attr/_cmp.py": 3297594878548179300, + "frontend/build/_app/immutable/chunks/2dOUpm6k.js": 11811637653151731489, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.py": 15726448028057859831, + "venv/lib/python3.13/site-packages/greenlet/slp_platformselect.h": 10428201768098464386, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/RECORD": 3649834117451110628, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_astype.py": 7497268667246913350, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/LICENSE": 6989336036499641077, + "backend/src/plugins/translate/__tests__/test_dictionary_crud.py": 11087404884677896422, + "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.py": 12771179468704314203, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_swaplevel.py": 3584770792383216626, + "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/__init__.py": 4761936067832638585, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/METADATA": 2810323639884879396, + "venv/lib/python3.13/site-packages/rsa/prime.py": 10820898885044917095, + "venv/lib/python3.13/site-packages/pygments/styles/nord.py": 8469538903422182155, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_comparisons.py": 2764694394636775220, + "venv/lib/python3.13/site-packages/greenlet/greenlet_compiler_compat.hpp": 465436185632243991, + "merge_spec.py": 5285945727297582371, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth2_session.py": 7076806577412682835, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_union_categoricals.py": 6843679254735807, + "frontend/build/_app/immutable/chunks/Bxli7MEx.js": 13142003860484323974, + "venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py": 14416705841810221305, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_join.py": 2585969863606902743, + "venv/lib/python3.13/site-packages/git/refs/reference.py": 2038789918710608481, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.cpython-313-x86_64-linux-gnu.so": 17089839649971421164, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_factorize.py": 17995673140216929494, + "frontend/src/services/connectionService.js": 16613277497380191542, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/RECORD": 9964194961227523130, + "venv/lib/python3.13/site-packages/pip/_internal/operations/check.py": 16650663913976953800, + "venv/lib/python3.13/site-packages/greenlet/tests/leakcheck.py": 10179165748538820557, + "venv/lib/python3.13/site-packages/anyio/_core/_signals.py": 1204322949639513552, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh17859.f": 13562390882286135024, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_one_public_routine.f90": 7957683453180939605, + "venv/lib/python3.13/site-packages/pip/_internal/cli/main.py": 9304034369762028739, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pipe.py": 7400355977900310233, + "venv/lib/python3.13/site-packages/uvicorn/protocols/__init__.py": 15130871412783076140, + "frontend/src/lib/stores/__tests__/mocks/navigation.js": 10782992736745205981, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_cython.py": 2933400021016349431, + "venv/lib/python3.13/site-packages/pandas/core/groupby/ops.py": 2036968385544457016, + "venv/lib/python3.13/site-packages/_pytest/terminalprogress.py": 5490989904517343259, + "venv/lib/python3.13/site-packages/starlette/_exception_handler.py": 9892172656847200124, + "venv/lib/python3.13/site-packages/websockets/typing.py": 13268801935274185287, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_transform.py": 5674175311567817854, + "venv/lib/python3.13/site-packages/websockets/extensions/base.py": 2457233337788783873, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/casting.py": 12226803454582253097, + "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py": 11613130235340753050, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/rethinkdb.py": 16413299263077970925, + "venv/lib/python3.13/site-packages/pandas/tests/extension/date/array.py": 18073740473413382768, + "venv/lib/python3.13/site-packages/PIL/__main__.py": 827665918723937817, + "venv/lib/python3.13/site-packages/more_itertools/__init__.py": 9858612105173082060, + "venv/lib/python3.13/site-packages/cryptography/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_chebyshev.py": 8054411443686791426, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/windows.py": 16452996247375488034, + "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.py": 6205675182996213576, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_dataframe.py": 14945138617962156057, + "venv/lib/python3.13/site-packages/pygments/lexers/_sql_builtins.py": 5906655440517434734, + "frontend/build/_app/immutable/nodes/7.DpRF6JXW.js": 11481132326681627493, + "venv/lib/python3.13/site-packages/_pytest/legacypath.py": 7512065138259788426, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed25519.py": 2285778903282667561, + "venv/lib/python3.13/site-packages/keyring/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/__init__.py": 8434616194114282088, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-expm1.csv": 8710594949902856122, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_iteration.py": 15247340610412364508, + "venv/lib/python3.13/site-packages/pygments/lexers/hexdump.py": 10665271113239513484, + "venv/lib/python3.13/site-packages/pandas/tseries/frequencies.py": 2262381949755250516, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_asymmetric.py": 6903778603779934572, + "venv/lib/python3.13/site-packages/websockets/legacy/exceptions.py": 16025779645869134942, + "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.py": 17055498803745289016, + "venv/lib/python3.13/site-packages/pip/_internal/commands/download.py": 7533669665002975823, + "venv/lib/python3.13/site-packages/pandas/core/reshape/api.py": 16364136812252343076, + "venv/lib/python3.13/site-packages/rsa/py.typed": 11041940105507257053, + "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pyi": 14150710436003340771, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_quantile.py": 4225791949463893856, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_loongarch64_linux.h": 6566098590441297326, + "venv/lib/python3.13/site-packages/pygments/lexers/mime.py": 7768464764849718825, + "venv/lib/python3.13/site-packages/websockets/imports.py": 1995356703044158461, + "venv/bin/httpx": 6651007446865716794, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/METADATA": 1684451390569121499, + "venv/lib/python3.13/site-packages/ecdsa/test_pyecdsa.py": 14542286382742044648, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexer.py": 14400766876373764268, + "venv/lib/python3.13/site-packages/pygments/lexers/php.py": 17636450950688953288, + "frontend/src/lib/components/dataset-review/__tests__/us2_semantic_workspace.ux.test.js": 3550956860258259058, + "venv/lib/python3.13/site-packages/pandas/tests/extension/json/__init__.py": 7877061921210840955, + "venv/lib/python3.13/site-packages/ecdsa/test_der.py": 7117444285097824495, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/filepost.py": 16698380375728063923, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/scipy_sparse.py": 2091608752719976909, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_map.py": 1115471561925426082, + "venv/lib/python3.13/site-packages/fastapi/websockets.py": 9910052969532075719, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_datetime.py": 7862725028865965163, + "venv/lib/python3.13/site-packages/pandas/core/indexes/api.py": 13750417165301467381, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_style.tpl": 3352086677417301397, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_common.py": 3559416690270529904, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.pyx": 649821942746469002, + "venv/lib/python3.13/site-packages/jeepney/tests/test_bus_messages.py": 4323093138307306385, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.pyx": 17138729833735744777, + "venv/lib/python3.13/site-packages/urllib3/contrib/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/RECORD": 9502524172853437600, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py": 3177663126928467950, + "venv/lib/python3.13/site-packages/referencing/_attrs.pyi": 10030665705880994025, + "backend/src/scripts/init_auth_db.py": 7729644769215024318, + "venv/lib/python3.13/site-packages/numpy/matlib.pyi": 8017902013455539981, + "venv/lib/python3.13/site-packages/httpx/_urls.py": 4219389176352582046, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi": 16042272072158438261, + "venv/lib/python3.13/site-packages/packaging/_parser.py": 13394093358675095309, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_contains.py": 5273759677032634868, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed448.py": 12264234876933578367, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/WHEEL": 7454950858448014158, + "venv/lib/python3.13/site-packages/httpx/__init__.py": 6383055693166097719, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/pretty.py": 2448541115957861214, + "venv/lib/python3.13/site-packages/numpy/lib/__init__.py": 12211229657513986410, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/__init__.py": 12906995259233516197, + "venv/lib/python3.13/site-packages/pandas/core/indexers/__init__.py": 1756397319068017890, + "venv/lib/python3.13/site-packages/keyring/credentials.py": 14515496037842724455, + "venv/lib/python3.13/site-packages/pillow.libs/libopenjp2-94e588ba.so.2.5.4": 2483101806045986252, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/models.py": 16636119441941357281, + "venv/lib/python3.13/site-packages/pandas/io/html.py": 2727247063231114153, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/utils.py": 7707707044867513457, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.py": 2569618488804767890, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/umath/svml/LICENSE": 9584102918819171104, + "venv/lib/python3.13/site-packages/pydantic/v1/color.py": 14545509649623975912, + "frontend/src/lib/stores/datasetReviewSession.js": 3915919349382526215, "venv/lib/python3.13/site-packages/authlib/common/urls.py": 13627272625165922482, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply_relabeling.py": 5600949862632014700, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_put.py": 8547660958963625817, + "venv/lib/python3.13/site-packages/passlib/tests/sample1.cfg": 9597283806285213340, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/base.py": 7110726987481713080, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_wrap.py": 17083394123203662880, + "venv/lib/python3.13/site-packages/authlib/oidc/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector_cpp.cpython-313-x86_64-linux-gnu.so": 16337157355618216738, + "venv/lib/python3.13/site-packages/jose/utils.py": 1926138853970725083, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/METADATA": 14543822270526146783, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_equivalence.py": 17843623343419137458, + "venv/lib/python3.13/site-packages/pygments/lexers/factor.py": 15788613154609197002, + "venv/lib/python3.13/site-packages/numpy/_core/overrides.py": 252940520997718714, + "frontend/src/routes/migration/mappings/+page.svelte": 4566398049366943407, + "backend/src/core/task_manager/persistence.py": 7896628499516364250, + "backend/src/services/clean_release/enums.py": 7365670144442292671, + "venv/lib/python3.13/site-packages/pygments/lexers/_mql_builtins.py": 2376212048709413586, + "venv/lib/python3.13/site-packages/numpy/random/_philox.cpython-313-x86_64-linux-gnu.so": 9916143171057774705, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_month.py": 14597470286884431085, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_index.py": 10971093361190283675, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-1.csv": 16078879475693341404, + "venv/lib/python3.13/site-packages/pygments/formatters/_mapping.py": 13335227913146542481, + "venv/lib/python3.13/site-packages/apscheduler/triggers/calendarinterval.py": 12481650996882675949, + "venv/lib/python3.13/site-packages/rapidfuzz/process.pyi": 6959881293311340830, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_musllinux.py": 2423350951094915747, + "frontend/src/routes/profile/+page.svelte": 17979719303737794427, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_pickle.py": 16196792378104396231, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np121.pkl.gz": 4708588000144933291, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_datetime.py": 4179112383995346046, + "venv/lib/python3.13/site-packages/keyring/testing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraypad.py": 4907193085512342162, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/chararray.pyi": 17340224970247832290, + "venv/lib/python3.13/site-packages/pandas/_libs/arrays.pyi": 12629806207276576877, + "venv/lib/python3.13/site-packages/pygments/lexers/tal.py": 447505725492547461, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/__init__.py": 14481526616887213120, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_3kcompat.h": 5377305123299457337, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/distributions.h": 17162252287980242413, + "venv/lib/python3.13/site-packages/authlib/jose/util.py": 14843923624043422219, + "venv/lib/python3.13/site-packages/numpy/_core/__init__.pyi": 2783897020308040960, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_date_range.py": 16128552513324017349, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_numpy_array_equal.py": 2089426719574307767, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_index.py": 12015738970771703312, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_deprecations.py": 16124509063246256847, + "backend/src/api/routes/__tests__/test_datasets.py": 14530822388357135497, + "backend/src/core/config_models.py": 4264626520931642357, + "venv/lib/python3.13/site-packages/annotated_types/py.typed": 15130871412783076140, + "backend/src/api/routes/assistant/_admin_routes.py": 3380446401429936332, + "venv/lib/python3.13/site-packages/jeepney/tests/__init__.py": 15130871412783076140, + "backend/git_repos/remote/test-repo.git/objects/ee/59553241ef084b83d3351ba4c33caf2ed9a840": 11587106407031972706, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/git/index/typ.py": 11661540784850363024, + "venv/lib/python3.13/site-packages/pandas/_libs/sas.pyi": 873957906443703977, + "venv/lib/python3.13/site-packages/greenlet/tests/test_generator.py": 14156412320760444199, + "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe_protocol.py": 12857147443873820285, + "venv/lib/python3.13/site-packages/numpy/tests/test__all__.py": 10689265249302359388, + "venv/lib/python3.13/site-packages/pygments/lexers/crystal.py": 3579547906957125755, + "venv/lib/python3.13/site-packages/secretstorage/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_structures.py": 16375837477294135345, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.py": 11225173272050906977, + "venv/lib/python3.13/site-packages/pandas/tests/test_nanops.py": 8709105600366913011, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/halffloat.h": 16913381775994652343, + "venv/lib/python3.13/site-packages/pydantic/_internal/_model_construction.py": 694471028676746110, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_abc.py": 10164280045317807365, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_value_attrspec.py": 7741590618654448240, + "venv/lib/python3.13/site-packages/fastapi/testclient.py": 2606236077497278662, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/modeline.py": 6194365452131561478, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/providers.py": 7267821734584612032, + "venv/lib/python3.13/site-packages/pandas/core/methods/to_dict.py": 4653628436393663485, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/common.py": 9354442374897565498, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/ops.py": 13451323660386712332, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/io/excel/_calamine.py": 13354427767660487637, + "backend/src/services/clean_release/compliance_execution_service.py": 3379474749353250488, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reshape.py": 10576230966727810832, + "backend/src/core/__tests__/test_superset_profile_lookup.py": 15590077829286810714, + "venv/lib/python3.13/site-packages/_pytest/_argcomplete.py": 3586514420674089892, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_mask.py": 5451866071307236899, + "venv/lib/python3.13/site-packages/fastapi/dependencies/models.py": 8626894872248212015, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending_distributions.pyx": 15302775787158991386, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/METADATA": 14180973198574731599, + "frontend/src/services/gitService.js": 7044825476968825357, + "backend/src/core/task_manager/graph.py": 1566662881946484762, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2": 6199424505696679197, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_inspect.py": 10573558345297891848, + "venv/lib/python3.13/site-packages/pandas/core/ops/dispatch.py": 7041711677358147534, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/resources.py": 11533706401259147051, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_join.py": 4213933887062089605, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_subclass.py": 10949229764696473939, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.pyf": 1361426175133186231, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/selectable.py": 14646717349365560549, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_copy.py": 12902342280706372953, + "venv/lib/python3.13/site-packages/pygments/lexers/typst.py": 8883289726601667104, + "venv/lib/python3.13/site-packages/_pytest/config/findpaths.py": 7867603756389940562, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_libgroupby.py": 9985769511561272200, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/exceptions.pyi": 9294219058621261971, + "venv/lib/python3.13/site-packages/pyasn1/type/namedval.py": 8273171503516887681, + "venv/lib/python3.13/site-packages/numpy/_core/numeric.py": 12261531924304979741, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling.py": 18221459425751198218, + "venv/lib/python3.13/site-packages/yaml/error.py": 5626374410579058436, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/METADATA": 317417773265681256, + "backend/src/api/routes/git/_gitea_routes.py": 14644218433873522624, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayprint.pyi": 2053388407376896377, + "venv/lib/python3.13/site-packages/numpy/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_array.py": 12514884344165043136, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth1_session.py": 1911069558956893789, + "venv/lib/python3.13/site-packages/cffi/cparser.py": 5246372077864988755, + "venv/lib/python3.13/site-packages/jsonschema/__main__.py": 18136546137047911521, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/websockets/extensions/__init__.py": 13067201313921664210, + "venv/lib/python3.13/site-packages/_pytest/assertion/rewrite.py": 2425990712758915831, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_no_public_entities.f90": 12296500845667555672, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rank.py": 5817408252050193538, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_convert.py": 13349243029437881949, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/redis.py": 11313520438600819390, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_numpy_compat.py": 9112925138440787555, + "venv/lib/python3.13/site-packages/pandas/tests/util/__init__.py": 15130871412783076140, + "backend/src/api/routes/migration.py": 6030566471113208264, + "venv/lib/python3.13/site-packages/httpcore/_sync/http2.py": 11167798787131917524, + "frontend/build/_app/immutable/nodes/0.BgrzyjjB.js": 4978926897476899846, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_shape_manipulation.pyi": 3533739482029983817, + "venv/lib/python3.13/site-packages/pytest/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/PcxImagePlugin.py": 1691405132377495336, + "venv/lib/python3.13/site-packages/tzlocal/windows_tz.py": 11696765495002954868, + "backend/src/services/clean_release/report_builder.py": 6953311294119437848, + "venv/lib/python3.13/site-packages/pandas/io/clipboards.py": 5183212433608887727, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/WHEEL": 2357997949040430835, + "frontend/src/routes/storage/backups/+page.svelte": 3437582895363203695, + "backend/src/plugins/translate/preview_prompt_helpers.py": 14303328860208074708, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/METADATA": 9041650844081036822, + "frontend/build/_app/immutable/chunks/DsAODWVW.js": 17093454341120407063, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/compat.py": 5871636909854554063, + "venv/lib/python3.13/site-packages/_pytest/warnings.py": 8633394774569709013, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/unrolled.py": 12651425412640834530, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.cpython-313-x86_64-linux-gnu.so": 13209491675968970365, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_index.py": 4908727359687606870, + "venv/lib/python3.13/site-packages/PIL/ImageTransform.py": 7825940907094338808, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/METADATA": 8985010079096958084, + "venv/lib/python3.13/site-packages/pandas/core/arrays/boolean.py": 8821671562937866269, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_replace.py": 8166203537440976347, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/terminal_theme.py": 13727399800263579809, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_conversion_utils.py": 1567199588622767277, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py": 10917162988625838981, + "venv/lib/python3.13/site-packages/httpcore/_utils.py": 3255581092836273826, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html.tpl": 12550218314797699304, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/secretstorage/defines.py": 9590959702075554048, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_frame.py": 2169381747938271444, + "backend/src/core/task_manager/task_logger.py": 12820629458199400734, + "venv/lib/python3.13/site-packages/jeepney/tests/test_low_level.py": 3579024079580951234, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.py": 15426828792217499138, + "venv/lib/python3.13/site-packages/cffi/parse_c_type.h": 9889309594462820068, + "venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py": 17461379236189029253, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/datonly.f90": 10495925225338237742, + "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py": 14275839608126658275, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_methods.py": 14881910898871057761, + "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.py": 11397521081216402594, + "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.pyi": 12275242654430570028, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_period.py": 3419297908709788013, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_update_delete.py": 1317337950621091021, + "venv/lib/python3.13/site-packages/pydantic/_internal/_docs_extraction.py": 5210114991139191634, + "venv/lib/python3.13/site-packages/PIL/ImageFilter.py": 10974179993355630141, + "venv/lib/python3.13/site-packages/yaml/loader.py": 18284689412149643959, + "venv/lib/python3.13/site-packages/greenlet/greenlet_allocator.hpp": 11041111583319491309, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_public_dtype_api_table.h": 8933016749764021181, + "venv/lib/python3.13/site-packages/pygments/lexers/_luau_builtins.py": 13690910306072616818, + "venv/lib/python3.13/site-packages/authlib/oauth1/errors.py": 1154126353335030046, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py": 15928258358116399067, + "venv/lib/python3.13/site-packages/pandas/core/window/rolling.py": 6316477981414191778, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_duplicates.py": 9830936017197354309, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/__init__.py": 12975130533860125272, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_map.py": 10769974470610467636, + "venv/lib/python3.13/site-packages/passlib/totp.py": 14590474309565233318, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_digest.py": 7988469818593853943, + "venv/lib/python3.13/site-packages/fastapi/middleware/wsgi.py": 3706578020491442104, + "venv/lib/python3.13/site-packages/pip/_internal/cli/progress_bars.py": 7584699216537478969, + "venv/lib/python3.13/site-packages/pygments/lexers/chapel.py": 7619603166448145621, + "venv/lib/python3.13/site-packages/pygments/lexers/cddl.py": 1303059539145221713, + "venv/lib/python3.13/site-packages/h11/py.typed": 12358286306858197118, + "venv/lib/python3.13/site-packages/PIL/ImageMode.py": 2361901066832921091, + "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.py": 1590647503660777156, + "venv/lib/python3.13/site-packages/apscheduler/job.py": 1858376671986108667, + "venv/lib/python3.13/site-packages/pygments/lexers/qlik.py": 3881663935630350152, + "venv/lib/python3.13/site-packages/numpy/_core/_struct_ufunc_tests.cpython-313-x86_64-linux-gnu.so": 13269324887178340397, + "frontend/src/services/storageService.js": 14408290095426695791, + "frontend/build/_app/immutable/nodes/20.DBjaXoRj.js": 8196764530953122359, + "venv/lib/python3.13/site-packages/anyio/abc/_eventloop.py": 2023190981288767991, + "venv/lib/python3.13/site-packages/dotenv/version.py": 13508433229287636267, + "venv/lib/python3.13/site-packages/passlib/tests/test_totp.py": 9150389741890748343, + "venv/lib/python3.13/site-packages/httpx/_transports/default.py": 8391302827983907910, + "venv/lib/python3.13/site-packages/rsa/pkcs1_v2.py": 3533398238491175229, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils_handlers.py": 556092537518149789, + "frontend/src/components/tools/ConnectionList.svelte": 13697819455163305485, + "frontend/build/_app/immutable/chunks/Bn0PWGM8.js": 3309051336151057391, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/utils.py": 9959199071030996679, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_infer_objects.py": 6769980076977692890, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_mask.py": 9654897363729130542, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_hist_method.py": 2109257663592928, + "venv/lib/python3.13/site-packages/pygments/formatters/terminal.py": 14380579279194052030, + "frontend/build/_app/immutable/nodes/31.DO2VkXtF.js": 13113501944684678688, + "backend/src/models/dataset_review_pkg/_finding_models.py": 17608383421420046491, + "venv/lib/python3.13/site-packages/click/formatting.py": 6694583529073729642, + "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.h": 5980733638838895780, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_is_homogeneous_dtype.py": 15378147393056707851, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_nonkeyword_arguments.py": 11676809974120007408, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py": 6067836157632573182, + "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.pyi": 2744889206277418389, + "venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py": 10265081145401348863, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/result.py": 8306497889916212025, + "venv/lib/python3.13/site-packages/python_multipart/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/styles/algol.py": 3040373291664740876, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.cpython-313-x86_64-linux-gnu.so": 7840846965295537844, + "venv/lib/python3.13/site-packages/pandas/tests/apply/conftest.py": 13798335354961261346, + "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/fields.py": 8923326131649837922, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth2_client.py": 6742098857947604729, + "venv/lib/python3.13/site-packages/pydantic/generics.py": 15019381413806459410, + "backend/src/models/dataset_review_pkg/_enums.py": 2243691494729792539, + "venv/lib/python3.13/site-packages/jose/exceptions.py": 8562012933958941039, + "backend/src/services/clean_release/stages/internal_sources_only.py": 15680055221844544848, + "venv/lib/python3.13/site-packages/ecdsa/test_ecdh.py": 3338834550392548171, + "venv/lib/python3.13/site-packages/dotenv/py.typed": 9796674040111366709, + "venv/lib/python3.13/site-packages/numpy/random/_mt19937.pyi": 15904463104068500379, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/size/foo.f90": 18067977329435720684, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_textreader.py": 388955620768371652, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reindex.py": 12459492712890114705, + "backend/tests/core/migration/test_archive_parser.py": 1909788158234142574, + "venv/lib/python3.13/site-packages/pandas/io/excel/_pyxlsb.py": 10035587889954731645, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/highway/LICENSE": 15636145745140922094, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/__init__.py": 260774374497653876, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/einsumfunc.py": 9944042006439149561, + "venv/lib/python3.13/site-packages/pandas/api/executors/__init__.py": 16954693181142010028, + "venv/lib/python3.13/site-packages/pandas/core/flags.py": 4690844374637430013, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.cpython-313-x86_64-linux-gnu.so": 6157288238855118017, + "backend/src/api/routes/translate/_preview_routes.py": 16234500901707822839, + "backend/src/services/git/_branch.py": 9229581689526423389, + "venv/lib/python3.13/site-packages/rsa/asn1.py": 3861968161559322784, + "venv/lib/python3.13/site-packages/pydantic/env_settings.py": 18250976255197243083, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_impl.py": 12056958946481531279, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/WHEEL": 5838667082726674838, + "venv/lib/python3.13/site-packages/click/parser.py": 13988544851350599869, + "venv/lib/python3.13/site-packages/numpy/lib/introspect.pyi": 12239924839764265507, + "venv/lib/python3.13/site-packages/pygments/lexers/apl.py": 16767474673619335683, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np126.pkl.gz": 9677994062701347206, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_to_xarray.py": 15341013524814290960, + "venv/lib/python3.13/site-packages/pygments/lexers/igor.py": 6506221273085723501, + "venv/lib/python3.13/site-packages/numpy/rec/__init__.py": 7467092880535460658, + "venv/lib/python3.13/site-packages/numpy/_pytesttester.py": 17737911110576747508, + "venv/lib/python3.13/site-packages/referencing/exceptions.py": 2535322334430110414, + "venv/lib/python3.13/site-packages/pandas/core/groupby/__init__.py": 3015263524218708013, + "venv/lib/python3.13/site-packages/ecdsa/test_malformed_sigs.py": 2606295770491528880, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/assertion_session.py": 18109848200936213195, + "venv/lib/python3.13/site-packages/pydantic/aliases.py": 14481980467204711659, + "venv/lib/python3.13/site-packages/numpy/lib/_version.py": 15294708923334674060, + "venv/lib/python3.13/site-packages/pandas/core/indexes/multi.py": 9115751979327393200, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/certs.py": 13366788049827102313, + "venv/lib/python3.13/site-packages/httpcore/_synchronization.py": 10997080307461682786, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/tokens_mixins.py": 9923721909178724437, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_conversion.py": 9159276697339089788, + "venv/lib/python3.13/site-packages/anyio/abc/_tasks.py": 16839170934610571514, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_common.py": 2223069451949206319, + "venv/lib/python3.13/site-packages/pandas/core/tools/datetimes.py": 13313374952742129866, + "venv/lib/python3.13/site-packages/numpy/random/_generator.pyi": 17167649524261942483, + "venv/lib/python3.13/site-packages/ecdsa/ellipticcurve.py": 15624510849346629163, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_parser.py": 8145921552236189367, + "venv/lib/python3.13/site-packages/pyasn1/type/char.py": 16518487114777185541, + "venv/lib/python3.13/site-packages/passlib/apps.py": 4633269405597895766, + "venv/lib/python3.13/site-packages/sqlalchemy/util/queue.py": 1289234691048379395, + "venv/lib/python3.13/site-packages/click/types.py": 6491651131669287534, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/__init__.py": 15130871412783076140, + "frontend/build/_app/immutable/chunks/iUWBOIA7.js": 17929809282036781253, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/WHEEL": 9788895026336324100, + "backend/src/plugins/git/llm_extension.py": 9347391649685791102, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_head_tail.py": 1906121476322841736, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_copy.py": 3114918520437707410, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_user_array.py": 8711697474193296376, + "venv/lib/python3.13/site-packages/pandas/core/groupby/generic.py": 4857114071347950833, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dim2.py": 5767600151459095125, + "backend/git_repos/remote/test-repo.git/objects/52/ec4a78c629329b8b572ec9d5205271c111baac": 18054296544295201561, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py": 7947998260347730581, + "backend/test_pat_api.py": 4206656943734821967, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/endpoint.py": 18260428408051728802, + "venv/lib/python3.13/site-packages/passlib/exc.py": 15559705487889657091, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_internal/locations/__init__.py": 8147012724727395488, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi": 18143251582901996072, + "venv/lib/python3.13/site-packages/pandas/core/indexes/period.py": 1181348384346239378, + "venv/lib/python3.13/site-packages/starlette/middleware/trustedhost.py": 8302185291610019926, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein_py.py": 1933257612725836484, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/kbkdf.py": 1359143503575245224, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_observance.py": 18178414966262524534, + "venv/lib/python3.13/site-packages/fastapi/security/base.py": 15674297568104917749, + "venv/lib/python3.13/site-packages/pandas/_libs/tslib.cpython-313-x86_64-linux-gnu.so": 10742615805788605480, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_doc.py": 7865575374100528149, + "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.py": 12355826783958402233, + "venv/lib/python3.13/site-packages/fastapi/dependencies/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/__version__.py": 14138134075118474134, + "venv/lib/python3.13/site-packages/numpy/core/defchararray.py": 11371045148957547088, + "frontend/src/lib/components/translate/TranslationRunProgress.svelte": 18041933754930100165, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval.py": 1228451182688768814, + "venv/lib/python3.13/site-packages/sqlalchemy/future/__init__.py": 17688444679736059779, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period.py": 9715843209581883915, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_invalid.py": 2762344062554575805, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_clearing_run_switches.py": 3020294789560975101, + "venv/lib/python3.13/site-packages/urllib3/poolmanager.py": 15431313180245757657, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/WHEEL": 14550174241288068152, + "venv/lib/python3.13/site-packages/charset_normalizer/models.py": 9917300217816275821, + "venv/lib/python3.13/site-packages/pydantic/v1/mypy.py": 17076910404409672245, + "venv/lib/python3.13/site-packages/attr/_typing_compat.pyi": 2823472697859409877, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/__init__.py": 5896924546560667519, + "venv/lib/python3.13/site-packages/numpy/ma/extras.py": 7559838309481824664, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py": 17060377178521307324, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/__init__.py": 12290228927283601067, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_scrypt.py": 17378030264314664250, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/core.py": 14576109242055156193, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append_common.py": 2377351120083195530, + "venv/lib/python3.13/site-packages/sqlalchemy/future/engine.py": 4639404437703898767, + "backend/src/services/clean_release/repositories/__init__.py": 9926779835387630811, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py": 853808724844451770, + "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.cpython-313-x86_64-linux-gnu.so": 5284769758475394095, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_astype.py": 4975959187621745360, + "venv/lib/python3.13/site-packages/packaging/version.py": 13470993734108007779, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/types.py": 13384984564922916167, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90": 7589258200398878529, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/RECORD": 16874983218652404911, + "venv/lib/python3.13/site-packages/pygments/lexers/_lilypond_builtins.py": 1766946089314631488, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/WHEEL": 7454950858448014158, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_numba.py": 8250377774229977284, + "venv/lib/python3.13/site-packages/pygments/lexers/ul4.py": 9580182474451220952, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_validators.py": 14178329676643700151, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/window/expanding.py": 14141093310397856286, + "venv/lib/python3.13/site-packages/pandas/core/computation/check.py": 16048939716341154196, + "venv/lib/python3.13/site-packages/starlette/middleware/errors.py": 1337979724602864440, + "venv/lib/python3.13/site-packages/passlib/utils/handlers.py": 10182859005870689026, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/errors.py": 2147619440137604907, + "venv/lib/python3.13/site-packages/pandas/_libs/pandas_datetime.cpython-313-x86_64-linux-gnu.so": 15874094681262240628, + "venv/lib/python3.13/site-packages/pandas/tests/apply/__init__.py": 15130871412783076140, + "backend/delete_running_tasks.py": 7776896567014157554, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_has_cy.py": 11979732417254560424, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x448.py": 3960540415600165213, + "backend/src/core/__tests__/test_native_filters.py": 823253235973274872, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_copy_deprecation.py": 16045426957636349035, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayobject.h": 4345432890284821918, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_update.py": 9098258267671647783, + "venv/lib/python3.13/site-packages/numpy/lib/_iotools.pyi": 12873243737872788349, + "venv/lib/python3.13/site-packages/numpy/random/lib/libnpyrandom.a": 1802449692091040396, + "venv/lib/python3.13/site-packages/packaging/utils.py": 3469957615920103670, + "venv/lib/python3.13/site-packages/gitdb/test/lib.py": 13294941660283059872, + "venv/lib/python3.13/site-packages/numpy/random/_mt19937.cpython-313-x86_64-linux-gnu.so": 85866616759631810, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_legacy.py": 9226692984248279094, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get.py": 6509102681661636928, + "scripts/scan_secrets.sh": 599173385111063848, + "venv/lib/python3.13/site-packages/uvicorn/loops/auto.py": 13086005977087619545, + "venv/lib/python3.13/site-packages/ecdsa/test_ecdsa.py": 17595025859105321562, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/nested_schemas.py": 4114645644496880787, + "venv/lib/python3.13/site-packages/bcrypt/__init__.py": 11415155211917181362, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api1.c": 8494977275215578998, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctan.csv": 4581185622290535488, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_regression.py": 4611768764484593979, + "venv/lib/python3.13/site-packages/pyasn1/codec/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_setops.py": 17350958906336246626, + "venv/lib/python3.13/site-packages/PIL/GimpGradientFile.py": 9777026720028891154, + "venv/lib/python3.13/site-packages/dateutil/tz/_factories.py": 7538654189286887518, + "venv/lib/python3.13/site-packages/pygments/lexers/make.py": 5345282370426217663, + "venv/lib/python3.13/site-packages/websockets/connection.py": 5168795926684067030, + "backend/src/core/auth/oauth.py": 9925908783612235042, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_explode.py": 18263694040596159591, + "venv/lib/python3.13/site-packages/pip/_internal/commands/cache.py": 1892313355296776523, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hashes.py": 8545380307201732345, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatter.py": 16499277113779414101, + "venv/lib/python3.13/site-packages/pillow.libs/liblzma-61b1002e.so.5.8.2": 11405063484938458842, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/jwk.py": 6458001384702613165, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/errors.py": 12935154705443176852, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/licenses/LICENSE": 13322127602069971876, + "venv/lib/python3.13/site-packages/greenlet/tests/test_contextvars.py": 6077482318964702132, + "frontend/src/components/DashboardGrid.svelte": 17669234585849525689, + "venv/lib/python3.13/site-packages/pygments/formatters/pangomarkup.py": 17135620955205945255, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/json.py": 14738429697319670035, + "backend/src/plugins/storage/plugin.py": 13669428465597027289, + "frontend/src/lib/components/layout/__tests__/test_taskDrawer.svelte.js": 15288009749873977442, + "venv/lib/python3.13/site-packages/pygments/styles/inkpot.py": 10516710471319090846, + "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.pyi": 12566839271245057531, + "frontend/src/routes/dashboards/[id]/components/DashboardTaskHistory.svelte": 8800115118935646769, + "frontend/src/lib/components/layout/TopNavbar.svelte": 16593656019460608311, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/build_tracker.py": 9389444024234384408, + "venv/lib/python3.13/site-packages/passlib/hash.py": 5238710192614935875, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_resolution.py": 6076761538303762297, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/index.py": 15694995431926521471, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/METADATA": 1862765380818895200, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_read.py": 8825002468745406134, + "venv/lib/python3.13/site-packages/numpy/version.pyi": 5075382895829889457, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_pickle.py": 8705060420848529055, + "venv/lib/python3.13/site-packages/PIL/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/core": 6622984507059115517, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/base.py": 7855217087916771270, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_regression.py": 16293647824734937323, + "frontend/src/components/MappingTable.svelte": 17937756426921453860, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_alter_axes.py": 11338712836362534969, + "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.pyi": 4587511199710546030, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler_py.py": 16189204441438239231, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_lexsort.py": 10394832796613968484, + "venv/lib/python3.13/site-packages/pygments/lexers/compiled.py": 8215290039991205531, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/WHEEL": 5990830714395966792, + "venv/lib/python3.13/site-packages/pip/_internal/commands/debug.py": 13528633965821062950, + "venv/lib/python3.13/site-packages/authlib/consts.py": 14226604841596736870, + "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.pyi": 5786150108623563787, + "venv/lib/python3.13/site-packages/starlette/middleware/gzip.py": 4321371046149637632, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_insert.py": 17383986803728810587, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py": 15130871412783076140, + "frontend/src/components/backups/BackupList.svelte": 14417854869668960310, + "venv/lib/python3.13/site-packages/pandas/tests/frame/common.py": 7426635866622531875, + "backend/src/plugins/translate/orchestrator_exec.py": 16849763962423948658, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/RECORD": 8104089156015954779, + "venv/lib/python3.13/site-packages/pydantic/typing.py": 6661252174518316760, + "venv/lib/python3.13/site-packages/PIL/ImageGrab.py": 11114379915762058868, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23533.f": 12558972205518833024, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/literal.py": 5641382379370828704, + "venv/lib/python3.13/site-packages/annotated_types/__init__.py": 7367790549779882749, + "venv/lib/python3.13/site-packages/pip/_internal/commands/index.py": 16715578868094112894, + "venv/lib/python3.13/site-packages/numpy/core/fromnumeric.py": 2136433423902202138, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_logical.py": 6242457515917145500, + "venv/lib/python3.13/site-packages/starlette/templating.py": 9242638124487797988, + "venv/lib/python3.13/site-packages/PIL/ImageTk.py": 17063969714816564357, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py": 15286893322149413712, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_invalid_arg.py": 610891871484326055, + "frontend/build/_app/immutable/chunks/tONMA49r.js": 7570814772386244995, + "venv/lib/python3.13/site-packages/_pytest/subtests.py": 14573790885130724739, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/py.typed": 9796674040111366709, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/base.py": 13634605726041717070, + "backend/src/services/resource_service.py": 1414472862621734532, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/jwt.py": 9642615960197748105, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/generate_umath_validation_data.cpp": 10880681943327843928, + "frontend/build/_app/immutable/nodes/32.BxAc7m7j.js": 12600465563142046438, + "backend/src/api/routes/translate/_job_routes.py": 217949482917696293, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/tornado.py": 6491136785917169991, + "venv/lib/python3.13/site-packages/websockets/legacy/handshake.py": 376095144657994920, + "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.py": 1809069379864985298, + "venv/lib/python3.13/site-packages/pandas/util/_doctools.py": 3766479022075611396, + "venv/lib/python3.13/site-packages/click/core.py": 16793133181718891193, + "venv/lib/python3.13/site-packages/cffi/backend_ctypes.py": 2042566833861899211, + "venv/lib/python3.13/site-packages/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py": 14824388798676578409, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/secrets.py": 17816662030600756342, + "venv/lib/python3.13/site-packages/_pytest/_io/__init__.py": 1510478951261938348, + "frontend/src/routes/reports/llm/[taskId]/+page.svelte": 14394593511269407852, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_cut.py": 7800833253841657380, + "venv/lib/python3.13/site-packages/yaml/cyaml.py": 9157893742065554693, + "backend/git_repos/remote/test-repo.git/objects/00/fcf77cecdf261aef91eda792ab3384a3339922": 1527330822006618217, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.pyf": 14358606965464506398, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_dict.py": 16252265723192124609, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/keywrap.py": 10424037418528425800, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/algorithms.py": 17106903014205241774, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsin.csv": 1665171675551692225, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.pyi": 13945478633722616250, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_matmul.py": 9323220618335072725, + "venv/lib/python3.13/site-packages/pygments/lexers/func.py": 12257919753150082714, + "frontend/build/_app/immutable/nodes/34.DNBovbvk.js": 17557855384122281040, + "frontend/build/_app/immutable/assets/9.tn0RQdqM.css": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/fixtures.py": 1442767529726551116, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_deprecations.py": 8310244516175542839, + "venv/lib/python3.13/site-packages/numpy/__config__.py": 15794735729525094396, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray_misc.pyi": 16219573290116396099, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/token.py": 4161760290367932793, + "venv/lib/python3.13/site-packages/greenlet/TThreadStateCreator.hpp": 9887867766190459665, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/WHEEL": 9131187629260560775, + "venv/lib/python3.13/site-packages/websockets/legacy/auth.py": 16685128086396181286, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log2.csv": 13979868713429167140, + "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.py": 9409852116555866847, + "venv/lib/python3.13/site-packages/httpcore/_sync/http11.py": 5947339024588907032, + "venv/lib/python3.13/site-packages/rsa/__init__.py": 14623742580410014505, + "venv/lib/python3.13/site-packages/PIL/_deprecate.py": 15076725072094847812, + "venv/lib/python3.13/site-packages/pip/_internal/models/direct_url.py": 4619204214785230341, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/METADATA": 4975169119201255384, + "venv/lib/python3.13/site-packages/pygments/lexers/teraterm.py": 16132676290138105984, + "venv/lib/python3.13/site-packages/passlib/utils/__init__.py": 11704122205941631503, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_base.py": 8678387511873412922, + "venv/lib/python3.13/site-packages/numpy/_distributor_init.pyi": 1308368231471324581, + "backend/git_repos/remote/test-repo.git/hooks/commit-msg.sample": 13435789416588783681, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/var_.py": 12699521091170347311, + "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.py": 1262543237476191714, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/__init__.py": 2791238481813240414, + "venv/lib/python3.13/site-packages/pydantic/validators.py": 8292819706488319845, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/METADATA": 5045041742469375466, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_inference.py": 5741637509343419942, + "venv/lib/python3.13/site-packages/numpy/lib/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pyasn1/type/opentype.py": 340146737126677655, + "venv/lib/python3.13/site-packages/gitdb/typ.py": 16374739906244805047, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cython.py": 14695721638531081573, + "venv/lib/python3.13/site-packages/ecdsa/_version.py": 17707102226558781833, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_join.py": 5982692266872978444, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py": 10894229569442891394, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.pyi": 16372699564794732463, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.c": 9318655518998644962, + "venv/lib/python3.13/site-packages/pandas/tests/test_multilevel.py": 10500316376521962061, + "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/test_uuid.py": 13955678427426850807, + "venv/lib/python3.13/site-packages/ecdsa/_compat.py": 13034064768731879065, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/warnings.py": 2552021607611112605, + "backend/src/services/reports/__tests__/test_report_normalizer.py": 5179524010104259469, + "frontend/src/services/taskService.js": 11368052191249479533, + "venv/lib/python3.13/site-packages/sqlalchemy/event/base.py": 18238453132776154057, + "venv/lib/python3.13/site-packages/pygments/lexers/_asy_builtins.py": 1665617945818958781, + "backend/src/plugins/translate/executor.py": 15866803979457728942, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcrypto-2e26a48f.so.3": 3658133521245392852, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f90continuation.f90": 17056591497459381871, + "venv/lib/python3.13/site-packages/pandas/core/computation/scope.py": 16859504937881548797, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/api.py": 9829629144629841277, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_constructors.py": 6727209833819502644, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/plugin.py": 18006138132596504101, + "venv/lib/python3.13/site-packages/passlib/handlers/cisco.py": 15004707325761459775, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils.py": 10362977406431569031, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/WHEEL": 15858869568085970864, + "venv/lib/python3.13/site-packages/fastapi/middleware/trustedhost.py": 4285622102799027823, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_api_info.py": 16491629653122827368, + "backend/src/core/superset_client/_user_projection.py": 16484323195475306883, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c": 17333399794446198412, + "frontend/build/_app/immutable/chunks/Bl4U_RKh.js": 1682675857949667017, + "frontend/src/routes/dashboards/[id]/components/DashboardHeader.svelte": 5572977872383619871, + "venv/lib/python3.13/site-packages/pandas/_libs/internals.pyi": 11662356720550765508, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_algorithms.py": 15219408648894515493, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE": 14316979437290727897, + "venv/lib/python3.13/site-packages/pandas/util/__init__.py": 4070574585004336241, + "venv/lib/python3.13/site-packages/_pytest/outcomes.py": 12782895027812738142, + "venv/lib/python3.13/site-packages/numpy/testing/__init__.py": 2831275254313750978, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/RECORD": 18314788057673509075, + "frontend/build/_app/immutable/nodes/11.hjip6Y23.js": 15351861233783503570, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/RECORD": 2024211292009381421, + "venv/lib/python3.13/site-packages/httpx/_transports/asgi.py": 12335675489870745729, + "backend/src/core/task_manager/__tests__/test_task_logger.py": 8175976176924379623, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_parse_dates.py": 7597935994945336408, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_common.h": 14732581790650403546, + "backend/src/models/dataset_review_pkg/__init__.py": 17789694606496652579, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/errors.py": 17581857895562494222, + "venv/lib/python3.13/site-packages/keyring/backends/null.py": 12740996416911604870, + "backend/src/scripts/clean_release_cli.py": 14467814161661747234, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_regression.py": 5652583095949741238, + "venv/lib/python3.13/site-packages/pandas/io/excel/_xlrd.py": 14430937200583666658, + "venv/lib/python3.13/site-packages/h11/_writers.py": 8585823094975171480, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_backend.py": 17793139799410518847, + "venv/lib/python3.13/site-packages/anyio/_core/_exceptions.py": 11969894261631227715, + "venv/lib/python3.13/site-packages/websockets/asyncio/client.py": 5694225754072131767, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/compat.py": 9758194415584776667, + "venv/lib/python3.13/site-packages/pip/_internal/commands/wheel.py": 16397273591245583356, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.pyi": 9282436354926617898, + "venv/lib/python3.13/site-packages/pandas/tests/config/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_datetimes.py": 7889573945417063183, + "venv/lib/python3.13/site-packages/PIL/SgiImagePlugin.py": 6916200065790425497, + "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/backend.py": 9072112660082423691, + "venv/lib/python3.13/site-packages/httpcore/_exceptions.py": 6716590711697088732, + "venv/lib/python3.13/site-packages/websockets/__main__.py": 11948055520555854616, + "venv/lib/python3.13/site-packages/pycparser/c_generator.py": 14922277346364091122, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/timeout.py": 1807228452024740476, + "venv/lib/python3.13/site-packages/pyasn1/__init__.py": 8089201217593738032, + "venv/lib/python3.13/site-packages/gitdb/db/ref.py": 1176701624460337371, + "venv/lib/python3.13/site-packages/numpy/lib/_datasource.py": 18136853038949826147, + "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/__init__.py": 18019562641348687547, + "venv/lib/python3.13/site-packages/pydantic/error_wrappers.py": 11273827025986230629, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/ec_key.py": 5961540064444749833, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nditer.py": 8341339612899904236, + "venv/lib/python3.13/site-packages/pandas/core/arrays/integer.py": 7775055011080964709, + "venv/lib/python3.13/site-packages/pandas/core/reshape/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/httpx/_config.py": 8800538050293498928, + "venv/lib/python3.13/site-packages/pandas/_libs/groupby.cpython-313-x86_64-linux-gnu.so": 14931394220112658268, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/computation/api.py": 17919983681396223380, + "venv/lib/python3.13/site-packages/pip/_internal/commands/install.py": 17631777189475433503, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_where.py": 14167991754625133668, + "venv/lib/python3.13/site-packages/PIL/PaletteFile.py": 4104323659569809769, + "venv/lib/python3.13/site-packages/PIL/PcdImagePlugin.py": 922809545463280224, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/uvicorn/_compat.py": 2593000889619875759, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunclike.pyi": 12215374902556647369, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_accuracy.py": 3904008607472505376, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_month.py": 1613806538807726074, + "venv/lib/python3.13/site-packages/pygments/formatters/groff.py": 6480830447984073574, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/requirements.py": 2362917888844255805, + "venv/lib/python3.13/site-packages/pip/_vendor/typing_extensions.py": 10158933506062403688, + "venv/lib/python3.13/site-packages/keyring/backend.py": 14185808762437650951, + "venv/lib/python3.13/site-packages/pygments/lexers/tcl.py": 10343667975436015855, + "venv/lib/python3.13/site-packages/pygments/lexers/idl.py": 11454152060351801696, + "venv/lib/python3.13/site-packages/pygments/lexers/vyper.py": 6477559246065994199, + "venv/lib/python3.13/site-packages/attrs/__init__.py": 9502968706258875910, + "venv/lib/python3.13/site-packages/jeepney/__init__.py": 1063800617763464773, + "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.pyi": 10077265709692546934, + "venv/lib/python3.13/site-packages/jsonschema/protocols.py": 847332300543820834, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.py": 1094797446508902066, + "venv/lib/python3.13/site-packages/dateutil/_version.py": 8279235793001245767, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/layout.py": 11554376181083053156, + "frontend/src/lib/components/ui/MultiSelect.svelte": 1475503303018683038, + "frontend/src/routes/admin/settings/llm/+page.svelte": 17057143078453873391, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_week.py": 5756727157937218128, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_logical_ops.py": 4438104524064299182, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.cpython-313-x86_64-linux-gnu.so": 14468234464367691795, + "venv/lib/python3.13/site-packages/pandas/compat/numpy/function.py": 18405870240333383757, + "venv/lib/python3.13/site-packages/pygments/lexers/ldap.py": 17455983150181177257, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/h11_impl.py": 1419719055394402145, + "venv/lib/python3.13/site-packages/pandas/io/parsers/__init__.py": 11752833703934347122, + "venv/lib/python3.13/site-packages/pygments/styles/monokai.py": 12381683616720952636, + "venv/lib/python3.13/site-packages/attrs/setters.py": 5775359015245600592, + "frontend/src/lib/components/dataset-review/SourceIntakePanel.svelte": 13922687579631047714, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_intervalindex.py": 5053113227747333759, + "frontend/build/_app/immutable/nodes/23.CFncMs3X.js": 14893442706232563898, + "frontend/build/_app/immutable/chunks/DsnmJJEf.js": 17416107759626312632, + "venv/lib/python3.13/site-packages/jose/constants.py": 10880650415673457194, + "venv/lib/python3.13/site-packages/pygments/lexers/jmespath.py": 7426464251945393925, + "venv/lib/python3.13/site-packages/iniconfig/exceptions.py": 16501270224069836264, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_floatingpoint_errors.py": 4301355720042319835, + "venv/lib/python3.13/site-packages/pip/_internal/models/index.py": 2839270935469791607, + "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.pyi": 9935587108964614958, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_dt_accessor.py": 2229616362327032253, + "venv/lib/python3.13/site-packages/numpy/core/_dtype.py": 2806449741844148191, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py": 8577756801858495168, + "venv/lib/python3.13/site-packages/sqlalchemy/util/tool_support.py": 4926732415520779988, + "venv/lib/python3.13/site-packages/pandas/util/_validators.py": 10201915528715968165, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_subplots.py": 4596089143375088346, + "venv/lib/python3.13/site-packages/git/repo/__init__.py": 6173604284490891047, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/foo_deps.f90": 14350239197565389508, + "frontend/svelte.config.js": 8473636210766821560, + "backend/src/api/routes/assistant/_command_parser.py": 2745692387994130159, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py": 2763962019423787504, + "frontend/src/lib/stores/sidebar.js": 8409084311045055153, + "venv/lib/python3.13/site-packages/pandas/tests/test_algos.py": 11860014896449369497, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_symbol.py": 10792387279408528886, + "backend/src/api/routes/git/_merge_routes.py": 2019590524299108754, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_tree.py": 15789443159423302291, + "venv/lib/python3.13/site-packages/pygments/lexers/rust.py": 8605690716125176329, + "venv/lib/python3.13/site-packages/pandas/api/typing/aliases.py": 11194270368147942988, + "backend/src/services/__tests__/test_llm_plugin_persistence.py": 2413185961042174999, + "venv/lib/python3.13/site-packages/numpy/ma/core.pyi": 4829891173366130328, + "venv/lib/python3.13/site-packages/anyio/_backends/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/passlib/utils/compat/__init__.py": 17369882236606423129, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.py": 3654583356259850174, + "venv/lib/python3.13/site-packages/pydantic/validate_call_decorator.py": 11130572937843915026, + "venv/lib/python3.13/site-packages/attrs/exceptions.py": 2871247302586505373, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dot.py": 16876560325283070437, + "venv/lib/python3.13/site-packages/urllib3/util/ssl_.py": 11720964010284185952, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_datetime.py": 11417836378782978454, + "frontend/src/services/adminService.js": 6375990585512014277, + "venv/lib/python3.13/site-packages/pydantic/_internal/_discriminated_union.py": 1818179910730401509, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_regression.py": 6742646011852052645, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/apps.py": 10758062668702044519, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_apply.py": 11248068903302454630, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_liboffsets.py": 14705822637347212540, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/columns.py": 8488246354818721406, + "venv/lib/python3.13/site-packages/pygments/lexers/codeql.py": 4254046699692048717, + "venv/lib/python3.13/site-packages/pygments/lexers/elpi.py": 5736689911777977822, + "venv/lib/python3.13/site-packages/PIL/ImageFile.py": 15765130658900167517, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/exceptions.py": 7518613450390473990, + "venv/lib/python3.13/site-packages/pygments/lexers/unicon.py": 1155454077052833821, + "frontend/src/lib/components/layout/TaskDrawer.svelte": 11803999908168836613, + "frontend/build/_app/immutable/nodes/35.DlfOaj-Z.js": 8253764223651453774, + "backend/src/core/database.py": 6142911633510653132, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_index.py": 13517883160584591966, + "venv/bin/activate": 14735074000123383125, + "venv/lib/python3.13/site-packages/h11/_state.py": 11398755254809751139, + "frontend/src/routes/settings/__tests__/settings_page.ux.test.js": 8756428349518151160, + "backend/src/api/routes/git/_deps.py": 12613530531412985767, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_case_when.py": 4133174129660834805, + "venv/lib/python3.13/site-packages/pygments/lexers/web.py": 11104721221615804497, + "backend/src/services/git/__init__.py": 14011752637167919253, + "backend/src/plugins/translate/__tests__/test_dictionary_correction.py": 7234669541203364452, + "venv/lib/python3.13/site-packages/_pytest/config/argparsing.py": 11156052029258891503, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/mod.pyi": 10591120373577657317, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_str_accessor.py": 4066094997616994612, + "venv/lib/python3.13/site-packages/_pytest/mark/structures.py": 12237146603608917650, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/__init__.py": 11157967642611612522, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.pyi": 51728851886466199, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi": 16934250836612138541, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/conftest.py": 8406022190801970256, + "venv/lib/python3.13/site-packages/pandas/io/formats/console.py": 10370334487585281127, + "frontend/src/components/PasswordPrompt.svelte": 13739802421315835398, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/METADATA": 6557531107125553027, + "backend/src/plugins/translate/__tests__/test_dictionary.py": 12126432977797732683, + "frontend/build/_app/immutable/chunks/FraETEdg.js": 1082619204392040869, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_function_base.py": 16624804022675533740, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/request.py": 17642741955458391050, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/INSTALLER": 17282701611721059870, + "frontend/src/lib/api/reports.js": 2328857814720054000, + "backend/src/plugins/translate/orchestrator_lang_stats.py": 5713612003349083382, + "venv/lib/python3.13/site-packages/cffi/_shimmed_dist_utils.py": 4824806157907607487, + "backend/src/core/__init__.py": 8673978627093182792, + "venv/lib/python3.13/site-packages/passlib/ext/django/models.py": 12386223748379294996, + "backend/tests/test_core_scheduler.py": 6589618269087393661, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py": 2492962412506082234, + "venv/lib/python3.13/site-packages/_pytest/warning_types.py": 10695097290648045427, + "backend/src/plugins/translate/prompt_builder.py": 9619706108488955349, + "backend/git_repos/remote/test-repo.git/objects/48/c3fde4d5fff5bf98e9bcaa5f328b199aa18292": 6121250272621110311, + "backend/git_repos/remote/test-repo.git/objects/d5/0298a42699f39e4601c0aa23eab07bcb0703c7": 1966124163198751340, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unique.py": 6234921572794573905, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_arrow.py": 10068866425352207113, + "venv/lib/python3.13/site-packages/pillow.libs/libxcb-64009ff3.so.1.1.0": 7322566017497612912, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_strings.py": 7168544888340296373, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix_py.py": 6653895521961110080, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_round_trip.py": 14684057343786032776, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_app.py": 4329794049222609894, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arithmetic.py": 3209727509456212551, + "venv/lib/python3.13/site-packages/smmap/__init__.py": 4557520096828426601, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename_axis.py": 13193478481758930128, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/_spdx.py": 17182188548503720124, + "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.pyi": 6534825471013879522, + "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.py": 151202984060101867, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccosh.csv": 5882025767947638305, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_categorical.py": 1329115818264527103, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/METADATA": 2624107638694809013, + "venv/lib/python3.13/site-packages/numpy/_core/_methods.py": 16805668769357938792, + "venv/lib/python3.13/site-packages/pip/_internal/cli/__init__.py": 11914508471759255927, + "venv/lib/python3.13/site-packages/numpy/_typing/_scalars.py": 17701938912160369190, + "frontend/eslint.config.js": 14296985543214702398, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/util.py": 4815334059917765852, + "venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py": 13594639612360346306, + "venv/lib/python3.13/site-packages/pandas/core/groupby/base.py": 16888506204910366302, + "backend/src/scripts/seed_superset_load_test.py": 12317164691465196579, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/abc.py": 6227782083136374888, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_array_to_datetime.py": 3467480435568672840, + "venv/lib/python3.13/site-packages/passlib/tests/test_registry.py": 15377817946543064293, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_conversion.pyi": 11580106670226366710, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_timedelta.py": 364160935264757405, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapped_collection.py": 11377280960650304531, + "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_tests.cpython-313-x86_64-linux-gnu.so": 442168371609469538, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_empty.py": 13006241382741353569, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_dtype.py": 1647214604678171565, + "venv/lib/python3.13/site-packages/PIL/_tkinter_finder.py": 14045496501810535432, + "venv/lib/python3.13/site-packages/pygments/styles/vim.py": 2621036947954523349, + "backend/src/api/routes/assistant/_llm_planner_intent.py": 16189718762013093878, + "venv/lib/python3.13/site-packages/pygments/formatters/svg.py": 11686316844190214147, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_use.f90": 2657057181044959432, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_insert.py": 15536994417338770457, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval_new.py": 3893421355544517336, + "venv/lib/python3.13/site-packages/jaraco/classes/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/rapidfuzz/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_ufunc.py": 5025605639221470838, + "venv/lib/python3.13/site-packages/pandas/tests/window/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_astype.py": 15384141282611151560, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.py": 12251264586530536622, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.pyi": 2700568594438441246, + "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.pyi": 17639497083041129225, + "venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py": 3095012838116670328, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/fixed_string.f90": 8654788371538544793, + "venv/lib/python3.13/site-packages/pip/_internal/locations/_distutils.py": 13559457590971821771, + "venv/lib/python3.13/site-packages/fastapi/utils.py": 15402403831700548944, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_function_base.pyi": 2792940598392356268, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py": 5616705208267050659, + "venv/lib/python3.13/site-packages/urllib3/util/ssltransport.py": 1175688460659745788, + "venv/lib/python3.13/site-packages/pip/_internal/commands/help.py": 4175797322922912326, + "venv/lib/python3.13/site-packages/pandas/core/ops/array_ops.py": 10061020479033421596, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/unistring.py": 18037463865539157041, + "venv/lib/python3.13/site-packages/PIL/_imaging.cpython-313-x86_64-linux-gnu.so": 1425926264271197601, + "venv/lib/python3.13/site-packages/pandas/core/internals/blocks.py": 6448406236246476333, + "venv/lib/python3.13/site-packages/websockets/speedups.c": 11800200877763464246, + "venv/lib/python3.13/site-packages/git/exc.py": 14278238357119816365, + "frontend/src/lib/components/translate/ScheduleConfig.svelte": 14007615494976044075, + "backend/src/api/routes/clean_release.py": 1984604735131504761, + "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.py": 5260748170237305627, + "backend/git_repos/remote/test-repo.git/description": 15262637409357137373, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/__init__.py": 11526998905763262175, + "venv/lib/python3.13/site-packages/starlette/status.py": 16192064443996706569, + "venv/lib/python3.13/site-packages/itsdangerous/_json.py": 4872267972688724005, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape.pyi": 2188930129299348535, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_os.h": 7883047506449405400, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename.py": 16148017223289654683, + "venv/lib/python3.13/site-packages/requests/certs.py": 16052880877156864882, + "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.pyi": 9854069702366900663, + "venv/lib/python3.13/site-packages/pandas/core/tools/timedeltas.py": 7221393731712061943, + "venv/lib/python3.13/site-packages/pandas/io/formats/printing.py": 3054136648863371285, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/text.py": 6096215585499446070, + "venv/lib/python3.13/site-packages/git/diff.py": 6691274015605451565, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/json.py": 15085143436056608404, + "venv/lib/python3.13/site-packages/pygments/lexers/solidity.py": 5071425960622305629, + "frontend/src/app.css": 3020720639702954139, + "venv/lib/python3.13/site-packages/passlib/handlers/pbkdf2.py": 16686685453577026643, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/random.pyi": 9425735458050148191, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/oracledb.py": 7158791242292470472, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_zips.py": 5184894018177148648, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/_gen_files.py": 6273314671315377131, + "backend/src/api/routes/dashboards/_listing_routes.py": 1750634467656242702, + "venv/lib/python3.13/site-packages/numpy/random/c_distributions.pxd": 16574161095720077595, + "venv/lib/python3.13/site-packages/attr/_version_info.py": 3850449653182103457, + "venv/lib/python3.13/site-packages/greenlet/tests/test_cpp.py": 10982107081267457286, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py": 16768579082462294745, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/WHEEL": 6514509858522675228, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/licenses/COPYING": 15270149301471737292, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/histograms.pyi": 2281259341486424755, + "venv/lib/python3.13/site-packages/jaraco/classes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/strings/object_array.py": 17631047188839635539, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unstack.py": 11834917665446437163, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dtypes.py": 4985057130807802395, + "venv/lib/python3.13/site-packages/anyio/_core/_sockets.py": 10525237827959090532, + "venv/lib/python3.13/site-packages/referencing/__init__.py": 10093824711214801917, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite_e.py": 8912795372542950449, + "venv/lib/python3.13/site-packages/dateutil/rrule.py": 398469817784230512, + "venv/lib/python3.13/site-packages/pygments/lexers/gcodelexer.py": 13712494327007684318, + "venv/lib/python3.13/site-packages/referencing/typing.py": 8296636464219681747, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_applicator_schemas.py": 8395157526634674143, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_misc.py": 8706265665356094801, + "venv/lib/python3.13/site-packages/pandas/core/construction.py": 10646956189518908985, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_arithmetic.py": 16045758521544316139, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsinh.csv": 13695545125848146013, + "venv/lib/python3.13/site-packages/numpy/random/_pcg64.cpython-313-x86_64-linux-gnu.so": 5759445712745875888, + "venv/lib/python3.13/site-packages/numpy/random/mtrand.cpython-313-x86_64-linux-gnu.so": 16146459667121787202, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/conftest.py": 25473177671450048, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/METADATA": 14608873187265321271, + "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py": 11718998093543631478, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_split_partition.py": 2715491071550545667, + "venv/lib/python3.13/site-packages/uvicorn/importer.py": 5449629250971153435, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py": 335201628064585213, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filter.py": 16846063190466267618, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/module_data_docstring.f90": 18245403117623015312, + "venv/lib/python3.13/site-packages/pygments/lexers/mips.py": 2078929831513352031, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pyodbc.py": 8175468565413744516, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/modula2.py": 6575895980250662093, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/__init__.py": 63497920264089252, + "venv/lib/python3.13/site-packages/passlib/handlers/windows.py": 8969739861295277439, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_construction.py": 10619501873883861745, + "backend/src/schemas/dataset_review_pkg/_composites.py": 1898056130177244572, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/aws.py": 1364297790170663941, + "venv/lib/python3.13/site-packages/jsonschema/validators.py": 3832874295500213309, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/gh_22819.pyf": 14717748358749934995, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/six.py": 1161259802604920635, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/dotenv.py": 5872848911499749500, + "venv/lib/python3.13/site-packages/numpy/lib/_datasource.pyi": 5894423389796456573, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_custom_dtypes.py": 12322100282959605055, + "venv/lib/python3.13/site-packages/websockets/asyncio/router.py": 15039562376314233669, + "venv/lib/python3.13/site-packages/pillow.libs/libXau-154567c4.so.6.0.0": 4340972632724893052, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/licenses/LICENSE": 7959949171752474553, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_constructors.py": 9815164146739254076, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py": 10093932131797570986, + "backend/src/api/routes/dashboards/_detail_routes.py": 12522696473630508473, + "venv/lib/python3.13/site-packages/fastapi/routing.py": 466387159689567541, + "venv/lib/python3.13/site-packages/secretstorage/__init__.py": 4535964275649588672, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data": 9599129816090399315, + "venv/lib/python3.13/site-packages/pygments/lexers/blueprint.py": 6370363226580199085, + "venv/lib/python3.13/site-packages/starlette/middleware/wsgi.py": 12506262030845301606, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_autocorr.py": 1092316868398391792, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_constructors.pyi": 4993066609272697419, + "venv/lib/python3.13/site-packages/pandas/_libs/index.pyi": 17679349633640606985, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_impl.py": 15673691977157706075, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_dispatcher.py": 2063037275809439611, + "backend/src/services/clean_release/__tests__/test_audit_service.py": 1679130450221264383, + "venv/lib/python3.13/site-packages/pandas/util/_test_decorators.py": 17930801441803090445, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_size.py": 14032390806503440750, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_crosstab.py": 6631711416347033281, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexing.py": 9988098331562906950, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/datasource.pyi": 16752667877866430577, + "venv/lib/python3.13/site-packages/numpy/tests/test_public_api.py": 11741922758622705449, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_delitem.py": 14336697139511118920, + "backend/src/plugins/translate/orchestrator_run_completion.py": 5945172469962331273, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/__init__.py": 6489437464172324468, + "venv/lib/python3.13/site-packages/rapidfuzz/_utils.py": 14374663810542207121, + "venv/lib/python3.13/site-packages/_pytest/__init__.py": 3769333500910148571, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/pyproject.py": 12684373765476932108, + "venv/lib/python3.13/site-packages/dateutil/relativedelta.py": 5903095963079475484, + "venv/lib/python3.13/site-packages/pandas/tests/series/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/styles/friendly_grayscale.py": 16840106759769837649, + "venv/lib/python3.13/site-packages/pandas/_libs/sparse.pyi": 12526865400413362140, + "frontend/build/index.html": 17585356623937136822, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/aiomysql.py": 12999962380333482189, + "backend/tests/scripts/test_clean_release_tui_v2.py": 16893299982170941753, + "venv/lib/python3.13/site-packages/numpy/exceptions.pyi": 13942486291715303235, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex.py": 16348619731902304844, + "venv/lib/python3.13/site-packages/fastapi/background.py": 16533963020573865222, + "venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py": 8698341051522816167, + "venv/lib/python3.13/site-packages/_pytest/_io/terminalwriter.py": 16852767249156780980, + "venv/lib/python3.13/site-packages/PIL/SunImagePlugin.py": 7661622930404688367, + "venv/lib/python3.13/site-packages/pandas/_config/__init__.py": 3577026529387563519, + "venv/lib/python3.13/site-packages/_pytest/junitxml.py": 5871715626272366062, + "venv/lib/python3.13/site-packages/numpy/_array_api_info.py": 9709101823494959741, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_transform.py": 18159263102656491974, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_formats.py": 2601044064677727807, + "venv/lib/python3.13/site-packages/sqlalchemy/util/langhelpers.py": 1246582552828542841, + "frontend/build/_app/immutable/nodes/19.DeFTT8VV.js": 10315863231029001542, + "venv/lib/python3.13/site-packages/pycparser/_c_ast.cfg": 2649342580905806389, + "venv/lib/python3.13/site-packages/rapidfuzz/utils_cpp.cpython-313-x86_64-linux-gnu.so": 16145829319198889672, + "backend/src/api/routes/tasks.py": 14935470445091298678, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/linalg.pyi": 269481015135399748, + "backend/src/plugins/translate/__tests__/test_orchestrator.py": 5221172340992429147, + "backend/alembic/versions/aa1b2c3d4e5f_drop_deprecated_translate_columns.py": 15433357059529111004, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_deprecations.py": 6015734648984591027, + "venv/lib/python3.13/site-packages/rsa/pem.py": 3118524021576082054, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py": 13454394827268856575, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/hooks.py": 9888227370911151341, + "venv/lib/python3.13/site-packages/greenlet/TUserGreenlet.cpp": 16258622236210473101, + "venv/lib/python3.13/site-packages/anyio/streams/text.py": 12507591237176920223, + "venv/lib/python3.13/site-packages/click/globals.py": 18353671282454331523, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_utils.pyi": 12312677132342982866, + "venv/bin/websockets": 12099863324306500376, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssltransport.py": 8148652555908329980, + "venv/lib/python3.13/site-packages/pydantic/json_schema.py": 15678278224696353978, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/refresh_token.py": 17927903035152872556, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils_pbkdf2.py": 17016810698578473773, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_docs.py": 17898499890274685198, + "venv/lib/python3.13/site-packages/PIL/PpmImagePlugin.py": 7107324729714470853, + "venv/lib/python3.13/site-packages/uvicorn/lifespan/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.pyi": 13083543811193933305, + "venv/lib/python3.13/site-packages/numpy/conftest.py": 8971879716683736515, + "venv/lib/python3.13/site-packages/greenlet/TMainGreenlet.cpp": 9489520001674089404, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs12.py": 5946672289916692272, + "venv/lib/python3.13/site-packages/pygments/console.py": 3540660073442483692, + "backend/src/services/git/_status.py": 11400014053359523156, + "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.py": 16859854811012717088, + "venv/lib/python3.13/site-packages/numpy/_core/defchararray.py": 15550230459945996915, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libselinux-0922c95c.so.1": 16516617710645376769, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.py": 1472021608501071055, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_snap.py": 4515844385819802784, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_getitem.py": 18202683049446557047, + "venv/lib/python3.13/site-packages/pytest_asyncio/__init__.py": 8162213292669110211, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_accumulations.py": 11428556645065711062, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reflection.py": 1799734694597531652, + "venv/lib/python3.13/site-packages/pydantic/_internal/_core_metadata.py": 5704137311404303574, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/auto.py": 12818217385551437286, + "venv/lib/python3.13/site-packages/pygments/styles/abap.py": 11543320440974964547, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/ddl.py": 5006132143435653867, + "venv/lib/python3.13/site-packages/keyring/completion.py": 1027436016070040568, + "backend/src/core/superset_client/_base.py": 17832258524611728315, + "venv/lib/python3.13/site-packages/pydantic/v1/errors.py": 11245135449608452720, + "venv/lib/python3.13/site-packages/pygments/lexers/clean.py": 17772169928835025327, + "backend/src/services/clean_release/approval_service.py": 15944489428318420008, + "venv/lib/python3.13/site-packages/certifi/__main__.py": 13417658012431779061, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/licenses/LICENSE": 3868190977070717994, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_concat.py": 1351478313334379336, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_callback.py": 1689177048527329209, + "venv/lib/python3.13/site-packages/jose/jwk.py": 2781217726458801910, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/licenses/LICENSE": 1424673738228762254, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_aix.h": 13316272821279308986, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/METADATA": 3474626408138844944, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/resource_owner_password_credentials.py": 9559295461619813890, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/_writer.py": 15053365792216907149, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_win32_console.py": 18092547253031555768, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/zookeeper.py": 4318643109309626098, + "venv/lib/python3.13/site-packages/attr/_cmp.pyi": 14443711863388606665, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_conversion.py": 6205112277291935767, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_hour.py": 13754646634123368797, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_py_util.py": 12836989066113400538, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/totp.py": 11888568940387752607, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_iterrows.py": 3791886201566670087, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_kurt.py": 2898715045429898166, + "venv/lib/python3.13/site-packages/sqlalchemy/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/ImageMath.py": 7395449734007598797, + "venv/lib/python3.13/site-packages/httpcore/_async/__init__.py": 491192949420297153, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/METADATA": 15225661995655327539, + "venv/lib/python3.13/site-packages/click/__init__.py": 9261629899056870564, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp.cpython-313-x86_64-linux-gnu.so": 14187094676876471848, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/exceptions.py": 2873060848310644552, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_stringdtype.py": 8381476847196129074, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_subclass.py": 12453252499544981915, + "venv/lib/python3.13/site-packages/pandas/io/iceberg.py": 13137128885014736233, + "venv/lib/python3.13/site-packages/PIL/_imagingmorph.pyi": 18222325750818585549, + "venv/lib/python3.13/site-packages/pip/_vendor/pkg_resources/__init__.py": 1677532855362260338, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_io.py": 4199750143323741326, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarrayobject.h": 7320510589635636690, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_coercion.py": 3551994404797560483, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_day.py": 2200008998040663785, + "venv/lib/python3.13/site-packages/pygments/styles/gh_dark.py": 7717523746712391809, + "venv/lib/python3.13/site-packages/pygments/lexers/webassembly.py": 17811970490091406867, + "venv/lib/python3.13/site-packages/pygments/lexers/zig.py": 470431199354307668, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_delete.py": 3485374671877063217, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_value_counts.py": 13270420637302622174, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/__init__.py": 10470644151952252331, + "venv/lib/python3.13/site-packages/PIL/XbmImagePlugin.py": 12256750478268246137, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598.f90": 1409455010615482779, + "venv/lib/python3.13/site-packages/numpy/tests/test_reloading.py": 17532116685329953400, + "backend/src/core/logger.py": 2069537382060633888, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending.pyx": 756189645898997788, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/android.py": 8607703347015852195, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_replace.py": 8784361132920473760, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_string.py": 13234624995755549329, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/WHEEL": 16784970174376303810, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/revocation.py": 5718403706971163960, + "venv/lib/python3.13/site-packages/pygments/lexers/_css_builtins.py": 17478340487186531268, + "backend/src/core/auth/jwt.py": 15575717945774687118, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/ecdsa/_rwlock.py": 7971972922423146201, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/structs.py": 4479429145468072604, + "venv/lib/python3.13/site-packages/jeepney/low_level.py": 6546206679858571608, + "venv/lib/python3.13/site-packages/psycopg2/_psycopg.cpython-313-x86_64-linux-gnu.so": 13471342289207537479, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py": 16386268386376675175, + "venv/lib/python3.13/site-packages/greenlet/greenlet_cpython_compat.hpp": 8396413256279803400, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_infer_objects.py": 16868563755487479303, + "backend/src/plugins/translate/preview_resolve_provider.py": 7349483761713339246, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/cache.py": 15037055977924167732, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/claims.py": 12645788445743898136, + "venv/lib/python3.13/site-packages/urllib3/util/__init__.py": 2848870790459558599, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/color_triplet.py": 17264298271771184, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress_bar.py": 15595912766961891283, + "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimelike.py": 4356700619632833878, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_take.py": 9847808489301498487, + "venv/lib/python3.13/site-packages/charset_normalizer/legacy.py": 1706599349563920562, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/table.py": 16856253349556498073, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_defchararray.py": 15135904718007443306, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/ocsp.pyi": 8666343427862784832, + "venv/lib/python3.13/site-packages/pydantic_core/core_schema.py": 8106298745123589494, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/_mapping.py": 13335227913146542481, + "venv/lib/python3.13/site-packages/pygments/lexers/fift.py": 7537570342576781911, + "frontend/src/components/StartupEnvironmentWizard.svelte": 13169478544862386336, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_overrides.py": 92685611709898742, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/background.py": 2105814165112049750, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE.PSF": 13208428090829817329, + "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp_impl.cpython-313-x86_64-linux-gnu.so": 11767771662042404251, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/auth.py": 912521029864094489, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_cte.py": 9274103293201050794, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_classes.py": 1440926036579216168, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numerictypes.py": 5719374287454872555, + "venv/lib/python3.13/site-packages/pandas/_config/localization.py": 11197430792343731762, + "venv/lib/python3.13/site-packages/packaging/specifiers.py": 2800449842990194437, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/precision.f90": 5518911932373565642, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pydantic/color.py": 1007422551673129827, + "venv/lib/python3.13/site-packages/sqlalchemy/log.py": 10101221585967121334, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_year.py": 1478857009124807836, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_nonunique_indexes.py": 15468078561381939405, + "frontend/public/vite.svg": 13568221685541582385, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/WHEEL": 8954358347596196608, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/__init__.py": 15971002342788501945, + "venv/lib/python3.13/site-packages/pygments/lexers/python.py": 8995519134978505744, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_linux.h": 15772978805500070727, + "frontend/src/app.html": 10207534912546549080, + "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/array.py": 13040461386712513615, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_round.py": 7912161077874611432, + "backend/src/api/routes/clean_release_v2.py": 12814311709297749713, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/cffi/commontypes.py": 11198107299826138465, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_formats.py": 13752366280743004410, + "backend/src/services/clean_release/stages/__init__.py": 11595548160903612178, + "venv/lib/python3.13/site-packages/ecdsa/test_sha3.py": 12459459298663975730, + "backend/src/plugins/translate/sql_generator.py": 900251269659959067, + "backend/git_repos/remote/test-repo.git/hooks/pre-commit.sample": 102291230237676317, + "backend/git_repos/remote/test-repo.git/objects/e9/1109abe29d5228675fecd6fc38a097147c090d": 15716642455993350072, + "backend/src/plugins/translate/orchestrator_runner.py": 1192466559618297192, + "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.pyi": 12052895927933479005, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_downcast.py": 10399746645741452200, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dtypes.py": 7550022036887594386, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_datetime_index.py": 15448494560143263150, + "venv/lib/python3.13/site-packages/pygments/lexers/j.py": 9766594937623918053, + "venv/lib/python3.13/site-packages/pandas/core/indexes/timedeltas.py": 13653393430363556843, + "venv/lib/python3.13/site-packages/httpcore/_backends/trio.py": 8394426709857317737, + "venv/lib/python3.13/site-packages/httpx/_auth.py": 1283189552342958900, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_network.py": 13159427333040109523, + "venv/lib/python3.13/site-packages/git/refs/tag.py": 9979537867501757110, + "venv/lib/python3.13/site-packages/attr/__init__.py": 14436902698244718082, + "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet.py": 10727595500186011102, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/authorization_server.py": 6858244712625056421, + "venv/lib/python3.13/site-packages/dotenv/__init__.py": 15464030923609356017, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/__init__.py": 1343779312297539596, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/measure.py": 18327922742808984428, + "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.pyi": 6723628093359375823, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/sum_.py": 7062070517544958546, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_period.py": 6255181868181182780, + "frontend/src/lib/auth/store.ts": 14669655798912377598, + "venv/lib/python3.13/site-packages/pip/_internal/utils/_log.py": 13011536737418204268, + "venv/lib/python3.13/site-packages/pydantic/deprecated/parse.py": 819235411736647763, + "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.py": 13574106461973044500, + "venv/lib/python3.13/site-packages/jeepney/io/asyncio.py": 13239992245695516007, + "venv/lib/python3.13/site-packages/pandas/tests/util/conftest.py": 11143177766735481660, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pop.py": 14885271225856187795, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_direct.py": 12680113511641199070, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/WHEEL": 5179340427739743287, + "venv/lib/python3.13/site-packages/starlette/types.py": 6372850128669191435, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py": 13021437050542691118, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_collections.py": 7772437080414835462, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/expression.py": 1248029135284283479, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_ops.py": 16346030307660048773, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/decl_class.py": 6328411894571735935, + "frontend/build/_app/immutable/nodes/14.1LDN7uQu.js": 17622239034125797956, + "venv/lib/python3.13/site-packages/cffi/verifier.py": 8473599757770867754, + "backend/src/api/routes/assistant/_resolvers.py": 1139669723103531814, + "venv/lib/python3.13/site-packages/jeepney/tests/secrets_introspect.xml": 10717038359151125278, + "backend/src/models/auth.py.bak": 7050478675360007060, + "venv/lib/python3.13/site-packages/git/index/__init__.py": 7874728461616959441, + "venv/lib/python3.13/site-packages/pip/__init__.py": 9449893869469432503, + "venv/lib/python3.13/site-packages/idna/compat.py": 9758194415584776667, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/RECORD": 12733654377842895588, + "venv/lib/python3.13/site-packages/gitdb/pack.py": 7828244200815444166, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py": 11139432164346742421, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/live.py": 8586058300967410643, + "venv/lib/python3.13/site-packages/pandas/tseries/offsets.py": 13750652921710295115, + "frontend/src/lib/components/translate/__tests__/test_translation_preview.svelte.js": 15091004230843271151, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/base_key.py": 10402800587070352851, + "venv/lib/python3.13/site-packages/httpcore/_sync/connection.py": 1539337414493486168, + "venv/lib/python3.13/site-packages/pygments/lexers/slash.py": 8320676560518108343, + "backend/src/api/routes/assistant/_routes.py": 6252816315418755346, + "backend/src/plugins/translate/orchestrator_aggregator.py": 14747269098451697881, + "backend/tests/services/dataset_review/test_superset_matrix.py": 5114797134613078913, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fft.pyi": 14454280827038327842, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_complex.py": 4290059580478881700, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_keywords.py": 11487954623214475915, + "venv/lib/python3.13/site-packages/passlib/tests/tox_support.py": 9200774792427691602, + "venv/lib/python3.13/site-packages/ecdsa/test_jacobi.py": 1249543655136186433, + "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/jws_eddsa.py": 833927568269372333, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/__init__.py": 15130871412783076140, + "frontend/src/lib/components/layout/sidebarNavigation.js": 1277379523298972456, + "frontend/src/components/storage/FileUpload.svelte": 14805293086068620697, + "backend/src/core/auth/repository.py": 7314530014494164270, + "backend/src/api/routes/translate/_schedule_routes.py": 18180097500694105300, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.pyf": 4660465266594480405, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/flow_control.py": 4822804840057360506, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.pyi": 10947021928755741366, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/RECORD": 11904769847385571600, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/quantile.py": 17341029201525362074, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/py.typed": 9796674040111366709, + "venv/lib/python3.13/site-packages/ecdsa/ssh.py": 17010933326090778158, + "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_generation_shared.py": 23345689300073549, + "venv/lib/python3.13/site-packages/_pytest/compat.py": 10046464101087327473, + "venv/lib/python3.13/site-packages/_pytest/skipping.py": 10561585274103209115, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/constrain.py": 6513876973118703677, + "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.pyi": 2029639875417260268, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.pyi": 8708830448398500675, + "venv/lib/python3.13/site-packages/numpy/_core/shape_base.pyi": 10225243791643474943, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/__init__.py": 4781700006554860887, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/use_data.f90": 14624074967635860696, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_filter.py": 2388681477981649268, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/regexopt.py": 16073437303824961930, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs12.pyi": 7801211423226951223, + "venv/lib/python3.13/site-packages/websockets/sync/messages.py": 15699091325918681038, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_converter.py": 7709581879446228856, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/chararray.pyi": 9683985931409244216, + "venv/lib/python3.13/site-packages/websockets/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_monotonic.py": 14648437502004398715, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_antijoin.py": 14435396877404907826, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/METADATA": 14070889071949105584, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_gen_files.py": 11149423183253618378, + "venv/lib/python3.13/site-packages/pandas/core/generic.py": 10174952306641057447, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/request.py": 10484280068366411541, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_xs.py": 11073752361562791165, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_function.py": 14142346897145169685, + "venv/lib/python3.13/site-packages/PIL/report.py": 9428754440615681790, + "venv/lib/python3.13/site-packages/pycparser/yacctab.py": 18018314512916353707, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/__init__.py": 3619091571279924576, + "venv/lib/python3.13/site-packages/pygments/lexers/tablegen.py": 2917626373516779857, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py": 3203921571999101793, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/WHEEL": 7454950858448014158, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_legacy.py": 12735901522735836481, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_truncate.py": 9623613657871046837, + "frontend/build/_app/immutable/nodes/8.B4G7-w5k.js": 18424156456357889561, + "venv/lib/python3.13/site-packages/gitdb/const.py": 14122142034887286559, + "venv/lib/python3.13/site-packages/starlette/authentication.py": 10905269573809129547, + "venv/lib/python3.13/site-packages/pip/_internal/network/xmlrpc.py": 5064964778750366059, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape_base.pyi": 5046405609030418900, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/result.py": 12262298207886620908, + "venv/bin/Activate.ps1": 1196103427939049710, + "backend/tests/test_dashboards_api.py": 2278794209397518464, + "frontend/e2e/tests/smoke.e2e.js": 11130903009452044456, + "venv/lib/python3.13/site-packages/authlib/oidc/core/userinfo.py": 17201158389976619872, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/charset_normalizer/cd.py": 8687667152597941021, + "frontend/src/lib/components/layout/Breadcrumbs.svelte": 10874106485242690185, + "frontend/build/_app/immutable/chunks/B3k-7f9N.js": 5233434304741777497, + "venv/lib/python3.13/site-packages/starlette/datastructures.py": 12097180838087229563, + "venv/lib/python3.13/site-packages/cryptography/hazmat/__init__.py": 6534110674636458866, + "venv/lib/python3.13/site-packages/pygments/lexers/bare.py": 18266858932333619451, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/histograms.pyi": 9255620158831643201, + "frontend/src/lib/ui/index.ts": 6883965364006817450, + "frontend/build/_app/immutable/assets/0.DGC1mqmM.css": 14666194691754113816, + "backend/src/api/routes/__tests__/test_profile_api.py": 1724401885939803766, + "backend/src/services/reports/__init__.py": 11818812865847046400, + "backend/src/plugins/translate/__tests__/test_sql_generator.py": 4467071942040481147, + "backend/src/scripts/clean_release_tui.py": 12384906707876740394, + "venv/lib/python3.13/site-packages/git/objects/tree.py": 10239027037919781330, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_ordered.py": 13827016781743929164, + "frontend/src/components/git/CommitHistory.svelte": 61942790722897211, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_polynomial.py": 3049262771098624825, + "venv/lib/python3.13/site-packages/pandas/tests/test_errors.py": 10116716310156374885, + "venv/lib/python3.13/site-packages/pandas/tests/construction/__init__.py": 15130871412783076140, + "backend/git_repos/remote/test-repo.git/objects/6f/85e20ce3ef98cf07a2f102498b71b9462552c5": 3012017807908986452, + "venv/lib/python3.13/site-packages/apscheduler/triggers/date.py": 9545774812177853725, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_constructors.pyi": 164538008396001632, + "venv/lib/python3.13/site-packages/pandas/core/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/dateutil/zoneinfo/rebuild.py": 1813298167468704155, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.py": 5735043585893007971, + "venv/lib/python3.13/site-packages/pip/_internal/cli/spinners.py": 7664056305019151182, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_random.py": 4179188367630060654, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_unique.py": 16285418015531891530, + "venv/lib/python3.13/site-packages/pandas/io/json/_normalize.py": 15938855324762935595, + "frontend/build/_app/immutable/chunks/CliV3vdy.js": 2705764711630653971, + "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.py": 11621577915097515056, + "venv/lib/python3.13/site-packages/more_itertools/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/io/formats/format.py": 3364941338174434661, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/uvicorn/middleware/wsgi.py": 9179913252056394958, + "backend/tests/test_translate_corrections.py": 1228642484417924184, + "venv/lib/python3.13/site-packages/keyring/compat/properties.py": 1562771115138037410, + "venv/lib/python3.13/site-packages/pandas/tests/libs/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_array.f90": 9711940829820746513, + "venv/lib/python3.13/site-packages/fastapi/_compat/model_field.py": 392699021682253569, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/py.typed": 10243553515949422671, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.py": 4440659352851541547, + "venv/lib/python3.13/site-packages/pandas/_libs/testing.cpython-313-x86_64-linux-gnu.so": 16758333404587340103, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timestamp.py": 10308799534491190078, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_can_hold_element.py": 5406431247116557833, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_utils.py": 2329717399120006435, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/asyncmy.py": 11808138763443525443, + "venv/lib/python3.13/site-packages/charset_normalizer/md.cpython-313-x86_64-linux-gnu.so": 1835299664481804968, + "venv/lib/python3.13/site-packages/pillow.libs/libwebpdemux-747f2b49.so.2.0.17": 7615380692384566654, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame.py": 16757570551941104401, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_value_counts.py": 9352540710376306520, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi": 14119032813912827271, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/uts46data.py": 1867240043777383728, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/RECORD": 3283324710045856319, + "venv/lib/python3.13/site-packages/greenlet/TThreadState.hpp": 15599148954148720463, + "venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py": 8657533190026387896, + "venv/lib/python3.13/site-packages/numpy/_core/numeric.pyi": 17785360553418386488, + "venv/lib/python3.13/site-packages/starlette/_utils.py": 8384087194580626069, + "venv/lib/python3.13/site-packages/pygments/lexers/asm.py": 13861952972248758785, + "frontend/src/routes/+page.svelte": 16267809857510759396, + "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.pyi": 5879290708972838025, + "frontend/src/components/EnvSelector.svelte": 3937920133889630193, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__main__.py": 6480933938145689964, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadb.py": 3109042476851791996, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/row.py": 9763935103055608874, + "backend/src/plugins/translate/service.py": 16455032944348136444, + "venv/lib/python3.13/site-packages/pygments/lexers/email.py": 4355246279485460399, + "venv/lib/python3.13/site-packages/websockets/http.py": 9406523480132204760, + "venv/lib/python3.13/site-packages/smmap/test/test_buf.py": 6910367739329016619, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS": 2987771679926243782, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/cells.py": 8299309127027168194, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimes.py": 8863748366184419339, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_http_headers.py": 13187746870209705951, + "venv/lib/python3.13/site-packages/click/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/METADATA": 11759004913807981650, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py": 17221368219485351394, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/RECORD": 6518517239543344179, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/__init__.py": 1437580234680265326, + "venv/lib/python3.13/site-packages/pip/_internal/utils/filetypes.py": 4415340223268174913, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexerrors.py": 4434868847540549873, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/simple.py": 11050636051019777317, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_spec_conformance.py": 16448324304809345527, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/screen.py": 6933591906297023845, + "venv/lib/python3.13/site-packages/pandas/tests/computation/test_compat.py": 11655000938111420543, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23879.f90": 13174951794239860144, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_federal.py": 16758212916399857971, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_skiprows.py": 8764515842253490735, + "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.py": 13446184230476677032, + "venv/lib/python3.13/site-packages/jaraco/functools/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py": 4559483361564472117, + "venv/lib/python3.13/site-packages/attr/exceptions.py": 11532465891810816417, + "venv/lib/python3.13/site-packages/git/refs/__init__.py": 16246686106073400519, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_array_ops.py": 6709502157317299163, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/q.py": 15426466074881683794, + "venv/lib/python3.13/site-packages/pygments/lexers/actionscript.py": 8829560731160428621, + "venv/lib/python3.13/site-packages/httpx/_multipart.py": 2181282475656908730, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_matrix_linalg.py": 2858987911347541177, + "venv/lib/python3.13/site-packages/pygments/lexers/archetype.py": 8995143180482517233, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/inout.f90": 1521999519583206538, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dropna.py": 17272771246760486947, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_sparse_accessor.py": 6774374030220202353, + "venv/lib/python3.13/site-packages/passlib/handlers/des_crypt.py": 15304795681178011265, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_unary.py": 7128845965976836334, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_constructors.py": 9478848417896027917, + "frontend/src/routes/translate/history/+page.svelte": 9504117584329454876, + "frontend/src/components/git/GitWorkspacePanel.svelte": 15810952071932060330, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/reporters.py": 10760889726586439724, + "backend/src/core/utils/superset_context_extractor/_recovery.py": 13298493527065532076, + "backend/src/core/plugin_base.py": 11649432288706102843, + "backend/src/models/dashboard.py": 8479838979214366697, + "backend/git_repos/remote/test-repo.git/objects/99/38d17c3f4199e53e4d4ebc253fe9c840caa505": 8414792171590415839, + "venv/lib/python3.13/site-packages/git/refs/head.py": 16271128287606894569, + "frontend/src/lib/components/reports/__tests__/reports_page.integration.test.js": 16463062768004950066, + "venv/lib/python3.13/site-packages/h11/_events.py": 13800479928851716538, + "backend/src/api/routes/translate/_metrics_routes.py": 13468336445030118059, + "venv/lib/python3.13/site-packages/pip/_internal/models/selection_prefs.py": 14700113736620314923, + "venv/lib/python3.13/site-packages/numpy/ma/testutils.pyi": 3899348061346041413, + "venv/lib/python3.13/site-packages/authlib/oidc/core/__init__.py": 16029653645350087968, + "venv/lib/python3.13/site-packages/pandas/tseries/__init__.py": 2648641118117738895, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py": 7423946096970870727, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_check_indexer.py": 2929031395357057009, + "venv/lib/python3.13/site-packages/pydantic/deprecated/config.py": 12280996227531856230, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arrow_interface.py": 5231449831607036031, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_reductions.py": 3575617237516305456, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_usecols_basic.py": 16382444980424010514, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_py_collections.py": 16825222625411635946, + "venv/lib/python3.13/site-packages/rapidfuzz/utils.pyi": 7400725037332728015, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/schema.py": 8358664670620586363, + "venv/lib/python3.13/site-packages/numpy/linalg/__init__.py": 15647756307608142577, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/foo.f": 6349567809926050379, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop.py": 15751236355035730554, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.py": 13853220736499979380, + "venv/lib/python3.13/site-packages/pandas/core/util/numba_.py": 3535857372998653863, + "venv/lib/python3.13/site-packages/PIL/ImagePath.py": 18323293175183057101, + "backend/src/plugins/translate/orchestrator_config.py": 1227864697811313446, + "venv/lib/python3.13/site-packages/pandas/core/interchange/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/starlette/formparsers.py": 9019779181100355515, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_reindex.py": 15680924058960901067, + "venv/lib/python3.13/site-packages/yaml/dumper.py": 11376022992370902766, + "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.py": 4940170059946018356, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/traversals.py": 1188980668212022868, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/ma/LICENSE": 6723325434476453127, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_raises.py": 9678418010293693275, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/themes.py": 6451091097727599550, + "venv/lib/python3.13/site-packages/pycparser/ply/cpp.py": 1665654017894222868, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/nonce.py": 18219168479887197733, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py": 8530403239168529668, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_compare.py": 171535659467902751, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_common.f": 5913807803369559749, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/hybrid.py": 5793056975404534989, + "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.pyi": 10638891136586198429, + "venv/lib/python3.13/site-packages/pydantic_settings/utils.py": 14396350455520958703, + "venv/lib/python3.13/site-packages/uvicorn/lifespan/on.py": 2866195356090164296, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py": 11657469008721907972, + "venv/lib/python3.13/site-packages/pygments/styles/trac.py": 13968346768164381833, + "frontend/src/components/TaskLogViewer.svelte": 11262767195652916566, + "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.pyi": 18253950285509265097, + "venv/lib/python3.13/site-packages/passlib/win32.py": 5311393450489898748, + "venv/lib/python3.13/site-packages/websockets/sync/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/_cryptography_key.py": 388489172928969873, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval.py": 8549680643865283913, + "venv/lib/python3.13/site-packages/pip/_internal/utils/compat.py": 1841193561752758336, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/WHEEL": 7127684561765977531, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE": 16499431172938528491, + "venv/lib/python3.13/site-packages/numpy/_typing/_extended_precision.py": 4201820494689784136, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_constructors.py": 2786493777737472376, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/x509/name.py": 15338066443257445241, + "venv/lib/python3.13/site-packages/httpcore/_backends/auto.py": 10488908050307887802, + "venv/lib/python3.13/site-packages/numpy/dtypes.py": 14128421398870718692, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/numpy/core/shape_base.py": 17925759824894973734, + "frontend/e2e/tests/git.e2e.js": 5892730242199126550, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/RECORD": 9803861100978586236, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/securetransport.py": 12104888897121018676, + "venv/lib/python3.13/site-packages/pygments/lexers/snobol.py": 414946539602526513, + "frontend/src/lib/components/health/PolicyForm.svelte": 16556369076015031814, + "backend/src/services/clean_release/compliance_orchestrator.py": 10585043190405940115, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log.csv": 221209423454319618, + "venv/lib/python3.13/site-packages/passlib/handlers/oracle.py": 12959175637015399815, + "venv/lib/python3.13/site-packages/apscheduler/executors/pool.py": 10344126569001032241, + "venv/lib/python3.13/site-packages/pandas/core/accessor.py": 1188716996113179524, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_pairwise.py": 2673370322448160842, + "venv/lib/python3.13/site-packages/pygments/styles/bw.py": 869630132791421268, + "venv/lib/python3.13/site-packages/uvicorn/_types.py": 3339389597716220585, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/base.py": 15872835330892203433, + "venv/lib/python3.13/site-packages/pygments/styles/emacs.py": 4540417536507245024, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/WHEEL": 16097436423493754389, + "frontend/src/lib/api/translate/dictionaries.js": 13928848549982189213, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_numpy.py": 222383250675686477, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_aggregate.py": 15038915958422670700, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/auto.py": 15947958974558543657, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_endian.h": 2761502363895204652, + "venv/lib/python3.13/site-packages/anyio/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/astype_copy.pkl": 4470155972891419163, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_argon2.py": 11345706437113628133, + "venv/lib/python3.13/site-packages/httpx/_transports/__init__.py": 5931944625631942279, + "venv/lib/python3.13/site-packages/pip/_internal/operations/prepare.py": 13514839972066861359, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_round.py": 1299086932398824299, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/wait.py": 6196757964348666082, + "venv/lib/python3.13/site-packages/attr/_make.py": 2348458829123990579, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/conftest.py": 4124267277224879990, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/ImageText.py": 15322496455608242001, + "venv/lib/python3.13/site-packages/git/objects/util.py": 691018806932355307, + "venv/lib/python3.13/site-packages/pygments/lexers/dax.py": 10868881057512002309, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_julian_date.py": 6868080396693831272, + "venv/lib/python3.13/site-packages/pluggy/_version.py": 7195758686717994475, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_iceberg.py": 1726421917529457420, + "venv/lib/python3.13/site-packages/pip/_internal/commands/uninstall.py": 11003472265011262515, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/METADATA": 11419135077900073883, + "venv/lib/python3.13/site-packages/numpy/ma/__init__.pyi": 1855615156913010107, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/__init__.py": 16123561007855424016, + "venv/lib/python3.13/site-packages/pandas/io/parsers/python_parser.py": 7297059565749331767, + "venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py": 932468678264370661, + "venv/lib/python3.13/site-packages/urllib3/_version.py": 839747992523118887, + "venv/bin/numpy-config": 17104619511899110252, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_readers.py": 2136472634307645842, + "frontend/src/lib/stores/__tests__/mocks/env_public.js": 2540122820969872185, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.py": 183817263146423033, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/x509.pyi": 17458894097215345719, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/licenses/LICENSE": 3868190977070717994, + "frontend/build/_app/immutable/nodes/22.DXHem7KH.js": 18074896018871089141, + "venv/lib/python3.13/site-packages/pandas/core/arrays/timedeltas.py": 7318851108932357510, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_sh_gcc.h": 3574353221233465215, + "venv/lib/python3.13/site-packages/authlib/oauth2/__init__.py": 13682931764896414722, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/filesize.py": 13234856188049714485, + "venv/lib/python3.13/site-packages/pydantic/plugin/__init__.py": 16803973597469216174, + "venv/lib/python3.13/site-packages/httpx/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pydantic/parse.py": 13750341403702794392, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/npyio.pyi": 14147712281086095231, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_period.py": 12593777226419608212, + "venv/lib/python3.13/site-packages/PIL/FontFile.py": 112053841436523305, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.cpython-313-x86_64-linux-gnu.so": 5441755022198996487, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/einsumfunc.pyi": 200771351328522379, + "venv/lib/python3.13/site-packages/pydantic/utils.py": 1533455264689378477, + "venv/lib/python3.13/site-packages/greenlet/tests/test_weakref.py": 6780204527427742510, + "venv/lib/python3.13/site-packages/numpy/_core/_asarray.py": 3468915833437229425, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_cov_corr.py": 13652339565475304679, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_describe.py": 14493300276133670380, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/type_check.pyi": 9177256991985898824, + "venv/lib/python3.13/site-packages/PIL/SpiderImagePlugin.py": 12234762087257163606, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py": 14946223589457772924, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/core/util/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/urllib3/util/retry.py": 17988032431204662756, + "venv/lib/python3.13/site-packages/pygments/lexers/css.py": 517586374264113606, + "venv/lib/python3.13/site-packages/pydantic/v1/version.py": 2758971830527526972, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arithmetic.py": 17043958604607035425, + "venv/lib/python3.13/site-packages/pandas/_libs/_cyutility.cpython-313-x86_64-linux-gnu.so": 3372660143512868174, + "venv/lib/python3.13/site-packages/httpcore/_sync/interfaces.py": 1532245423878428608, + "frontend/build/_app/immutable/nodes/30.Da464piG.js": 12939890826029944794, + "backend/src/core/superset_client/__init__.py": 14560361361214953450, + "backend/src/plugins/backup.py": 9697280948668596192, + "venv/lib/python3.13/site-packages/pandas/core/interchange/utils.py": 11401792151107207834, + "backend/src/plugins/translate/service_datasource.py": 10240814407336170041, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py": 10379929935087904974, + "backend/src/services/clean_release/audit_service.py": 3614108458934359126, + "venv/lib/python3.13/site-packages/pygments/lexers/dylan.py": 16246299896150112270, + "backend/tests/test_translate_scheduler_guard.py": 13396608843530344156, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_astype.py": 8633811117672837999, + "venv/lib/python3.13/site-packages/numpy/lib/__init__.pyi": 1537495515469182226, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/websockets/asyncio/messages.py": 6848925149930551457, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/_internal_utils.py": 4091305333167213279, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/__init__.py": 13331622784664204066, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py": 2833016678144975576, + "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.pyi": 15130871412783076140, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.APACHE": 11041304845352917971, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/core/methods/describe.py": 17006836623161521092, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_libfrequencies.py": 16302676625743291594, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_set_name.py": 9094608008538024609, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/fromnumeric.pyi": 6708743411405679061, + "venv/lib/python3.13/site-packages/greenlet/TGreenlet.cpp": 4121171004360760212, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/packages.py": 11689856177725665974, + "venv/lib/python3.13/site-packages/charset_normalizer/__init__.py": 12412600648990392633, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/take.py": 3170415198452493008, + "frontend/src/routes/translate/+page.svelte": 15284819686967093613, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/types.py": 12298292195929126306, + "backend/src/api/routes/mappings.py": 7071183920129492341, + "venv/lib/python3.13/site-packages/passlib/crypto/digest.py": 15890463288512025688, + "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimes.py": 10585034798097337148, + "venv/lib/python3.13/site-packages/ecdsa/test_rw_lock.py": 1572802971060634697, + "venv/lib/python3.13/site-packages/ecdsa/der.py": 9374566189581725372, + "venv/lib/python3.13/site-packages/bcrypt/__about__.py": 11196369962175692786, + "venv/lib/python3.13/site-packages/pygments/styles/rainbow_dash.py": 12236746494379891602, + "venv/lib/python3.13/site-packages/pygments/styles/rrt.py": 9490973359165080360, + "backend/tests/test_translate_scheduler_execution.py": 16687335504320865378, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sin.csv": 1443087417808027268, + "backend/src/plugins/translate/dictionary_filter.py": 18047895364995512274, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.pyi": 9003794318682878317, + "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.pyi": 6576645822227068922, + "venv/lib/python3.13/site-packages/pycparser/__init__.py": 16499940957411260458, + "venv/lib/python3.13/site-packages/PIL/_imagingtk.pyi": 18222325750818585549, + "backend/src/core/utils/dataset_mapper.py": 13413818977924738147, + "frontend/src/routes/dashboards/[id]/components/DashboardGitManager.svelte": 14665567599893731601, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py": 5467043641314706635, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/aioodbc.py": 12704729841432715246, + "venv/lib/python3.13/site-packages/requests/_internal_utils.py": 4091305333167213279, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/rec.pyi": 14615751247243515028, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/resource_protector.py": 9147671710440653567, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/containers.py": 5969831151685016772, + "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.pyi": 1521384724035973688, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/WHEEL": 16097436423493754389, + "frontend/src/routes/git/+page.svelte": 14169430923974356254, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/prompt.py": 9561640601516933380, + "venv/lib/python3.13/site-packages/numpy/_core/_internal.pyi": 6840742333427164920, + "venv/lib/python3.13/site-packages/pyasn1/type/useful.py": 10313387611927801339, + "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pxd": 14261507718442469988, + "venv/lib/python3.13/site-packages/pandas/api/internals.py": 5188134630996087511, + "frontend/build/_app/env.js": 8815854342083926790, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_sql.py": 3901697517087276687, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_byteswap.py": 5090287710338893350, + "venv/lib/python3.13/site-packages/numpy/core/umath.py": 4476812261188108155, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/__main__.py": 487896753130729554, + "venv/lib/python3.13/site-packages/pandas/tests/api/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_api.py": 10514120091521589282, + "venv/include/site/python3.13/greenlet/greenlet.h": 16216120538647882082, + "backend/src/services/__tests__/test_resource_service.py": 14435810729789665272, + "venv/lib/python3.13/site-packages/keyring/backends/chainer.py": 12526770797516553385, + "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/__init__.py": 15991623390785635773, + "backend/src/schemas/dataset_review_pkg/_dtos.py": 13301885458265748263, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/errors.py": 12777931960088171310, + "venv/lib/python3.13/site-packages/pip/_internal/models/target_python.py": 422323125651848065, + "venv/lib/python3.13/site-packages/pandas/core/apply.py": 17080038932032802984, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/__init__.py": 8093984834068317399, + "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/__init__.py": 8705044848308260480, + "venv/lib/python3.13/site-packages/numpy/lib/_version.pyi": 10804755840730584157, + "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_stat_reductions.py": 11569525436540769152, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_operators.py": 12686786829906210592, + "venv/lib/python3.13/site-packages/jaraco/context/__init__.py": 10975097417056278427, + "venv/lib/python3.13/site-packages/apscheduler/executors/twisted.py": 2695602672085615762, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/rec.pyi": 9140655366667421698, + "venv/lib/python3.13/site-packages/numpy/lib/format.py": 14971167445246135440, + "venv/lib/python3.13/site-packages/passlib/pwd.py": 14727089240749133060, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polynomial.py": 777287409151500309, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/base.py": 14734745827490220667, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/impl.py": 4687705587963980337, + "venv/lib/python3.13/site-packages/greenlet/tests/test_extension_interface.py": 11693583111513457974, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/__init__.pyi": 10548370574281981402, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/bitwise_ops.pyi": 751490187478960891, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/identity.py": 326059526167697655, + "venv/lib/python3.13/site-packages/charset_normalizer/constant.py": 3070316197495651997, + "venv/bin/py.test": 2593802493707545818, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_reductions.py": 8258332365408303156, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_categorical_equal.py": 7942323667866280872, + "frontend/src/lib/stores/__tests__/mocks/environment.js": 16653269108049932073, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/AUTHORS": 10073225185964928425, + "venv/lib/python3.13/site-packages/rsa/parallel.py": 4968566722541778371, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta_range.py": 15081095442418142820, + "frontend/src/routes/admin/roles/+page.svelte": 1343040884276845574, + "backend/src/api/routes/git/_repo_lifecycle_routes.py": 6605622512801609889, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/eoo.py": 5126566925877730303, + "venv/lib/python3.13/site-packages/starlette/middleware/sessions.py": 11719848399661632707, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_comparison.py": 11333735187162513512, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_take.py": 3281943380342853080, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_halfyear.py": 587199217232946404, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/index_tricks.py": 10051261976498916957, + "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.cpython-313-x86_64-linux-gnu.so": 13917011878259810696, + "venv/lib/python3.13/site-packages/pygments/lexers/text.py": 10572727755736597715, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_equals.py": 6429101196875333574, + "venv/lib/python3.13/site-packages/pandas/tests/test_expressions.py": 16378925536309828288, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_struct_accessor.py": 6567451952835390075, + "venv/lib/python3.13/site-packages/pygments/lexers/pascal.py": 5992422120590352714, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_ewm.py": 13789842684407148830, + "venv/lib/python3.13/site-packages/PIL/__init__.py": 10393984345353752724, + "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.pyi": 15034917167424979097, + "venv/lib/python3.13/site-packages/pygments/lexers/inferno.py": 9056274496469409594, + "venv/lib/python3.13/site-packages/pygments/lexers/r.py": 14263133575134645005, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reduce.py": 14976712507854054776, + "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.py": 14696841109142419143, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/RECORD": 7844752443022514265, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_ssl_constants.py": 14834514243239777933, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/implicit.py": 5400555157488808948, + "venv/lib/python3.13/site-packages/pandas/tests/resample/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/packaging/metadata.py": 9168255069163141470, + "frontend/src/lib/components/translate/__tests__/test_bulk_replace_modal.svelte.js": 6802604718120565742, + "frontend/src/routes/profile/__tests__/fixtures/profile.fixtures.js": 2726190101012864209, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_datetime.py": 3499461736474530062, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/base.py": 16714704323747942110, + "venv/lib/python3.13/site-packages/python_multipart/exceptions.py": 5971058243218049183, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_ops.py": 3875181889032080505, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_inclusive.py": 1389676378885557519, + "venv/lib/python3.13/site-packages/passlib/handlers/mysql.py": 11165762635383041460, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/__init__.py": 6532094538918910232, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_reductions.py": 3227705565578016389, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_map.py": 4761668276463940924, + "venv/lib/python3.13/site-packages/cffi/_imp_emulation.py": 17246725564704952212, + "venv/lib/python3.13/site-packages/pip/_internal/cli/command_context.py": 9426036980437931533, + "venv/lib/python3.13/site-packages/PIL/FitsImagePlugin.py": 618556384932056120, + "venv/lib/python3.13/site-packages/git/repo/base.py": 13975465345838683533, + "venv/lib/python3.13/site-packages/pydantic/type_adapter.py": 10162988530225478692, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/comparisons.pyi": 4158883009780474220, + "venv/lib/python3.13/site-packages/numpy/ma/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/CObjects.cpp": 3454797518210223926, + "venv/lib/python3.13/site-packages/pygments/lexers/ooc.py": 14540210212631538007, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cos.csv": 12896264205153363031, + "venv/lib/python3.13/site-packages/pydantic/_internal/_import_utils.py": 4029157986210295967, + "venv/lib/python3.13/site-packages/urllib3/http2/probe.py": 4069929463671970243, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_select_dtypes.py": 3530442956913622572, + "backend/git_repos/remote/test-repo.git/hooks/update.sample": 8204581436750313444, + "venv/lib/python3.13/site-packages/_pytest/raises.py": 8188402179490363767, + "backend/git_repos/remote/test-repo.git/objects/38/0395343915d386a8d58774cc6c6677205065e8": 5481382229802988191, + "venv/lib/python3.13/site-packages/PIL/IptcImagePlugin.py": 14329453455296600824, + "backend/src/models/__tests__/test_report_models.py": 14916807060334836854, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_strings.py": 17398806130402098912, + "venv/lib/python3.13/site-packages/pydantic/alias_generators.py": 5991580234179623842, + "venv/lib/python3.13/site-packages/PIL/PalmImagePlugin.py": 13008868719182630239, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_is_full.py": 9577318758326279694, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_bin_groupby.py": 14831211659124136806, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_label_or_level_utils.py": 3423644585913150841, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_fillna.py": 9297637486401098611, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_complexcompat.h": 5001035774408914877, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_scalar_compat.py": 1306236983392218709, + "venv/lib/python3.13/site-packages/pygments/lexers/mojo.py": 8616283481592061285, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/base.py": 10205051944814316388, + "venv/lib/python3.13/site-packages/itsdangerous/signer.py": 3694661094753475840, + "backend/git_repos/remote/test-repo.git/hooks/pre-push.sample": 8029776382195911768, + "venv/lib/python3.13/site-packages/pygments/lexers/csound.py": 2175454840368374515, + "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.py": 548928042543500365, + "venv/lib/python3.13/site-packages/pygments/lexers/_scheme_builtins.py": 5722444920479775453, + "frontend/test-results/translation.e2e.js-Transla-49baf-te-a-translation-job-via-UI-chromium/video.webm": 3868075359950286684, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/common/pythoncapi-compat/COPYING": 3474104874623144155, + "venv/lib/python3.13/site-packages/apscheduler/triggers/interval.py": 3964550092873794670, + "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pxd": 9126772093305708921, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77fixedform.f95": 10898637805043867370, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/__init__.py": 3969805865048730851, + "backend/src/plugins/translate/_llm_http.py": 13936219393017392941, + "frontend/build/_app/immutable/nodes/3.BwAzxKYV.js": 8233995785147305685, + "venv/lib/python3.13/site-packages/cffi/vengine_gen.py": 6582900613549620556, + "venv/lib/python3.13/site-packages/pip/_internal/models/search_scope.py": 9282393652860589543, + "venv/lib/python3.13/site-packages/pandas/core/indexes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_scalar.py": 2434773821902319678, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/METADATA": 14126682758562488244, + "venv/lib/python3.13/site-packages/_pytest/_io/pprint.py": 11202117031437223527, + "venv/lib/python3.13/site-packages/dateutil/parser/isoparser.py": 18015430204844361067, + "venv/lib/python3.13/site-packages/greenlet/greenlet_internal.hpp": 13173115501223013762, + "venv/lib/python3.13/site-packages/numpy/core/overrides.py": 11228841530565061867, + "venv/lib/python3.13/site-packages/pygments/lexers/meson.py": 14659991367778746658, + "frontend/src/components/git/BranchSelector.svelte": 12975425183125935169, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/authorization_server.py": 724388765673082720, + "venv/lib/python3.13/site-packages/numpy/f2py/__version__.pyi": 9759534743796867731, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite.py": 3628609433179387988, + "venv/lib/python3.13/site-packages/pandas/core/internals/ops.py": 4740509262379451243, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_sample.py": 4641567828238191927, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_transpose.py": 1686867045869336370, + "venv/lib/python3.13/site-packages/secretstorage/collection.py": 17903258621904912493, + "venv/bin/normalizer": 10972303485259566838, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/resample/conftest.py": 1643522609234951699, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/WHEEL": 13232065379159720345, + "venv/lib/python3.13/site-packages/uvicorn/_subprocess.py": 13965754246493254447, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-1.csv": 2202360898679162333, + "venv/lib/python3.13/site-packages/psycopg2/_range.py": 7184417109068014771, + "venv/lib/python3.13/site-packages/passlib/context.py": 10362592407593745107, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/content": 12465123883296161860, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reserved_words.py": 8022121780993497971, + "frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte": 16210598715819306972, + "venv/lib/python3.13/site-packages/websockets/legacy/protocol.py": 762692033188392424, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_arithmetic.py": 11620920548943067949, + "backend/src/core/middleware/__init__.py": 13917394130915533209, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/value_attrspec/gh21665.f90": 11945588837236968550, + "frontend/build/_app/immutable/chunks/B1mU1HkL.js": 434639091648147151, + "backend/git_repos/remote/test-repo.git/objects/3e/05ec6ef6504483c4bc7d00f0d0272c38ec661f": 11182016264585388842, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/url.py": 9692934100602135106, + "venv/lib/python3.13/site-packages/_pytest/mark/expression.py": 1821437025841201912, + "venv/lib/python3.13/site-packages/anyio/pytest_plugin.py": 4165280709813718099, + "venv/lib/python3.13/site-packages/numpy/rec/__init__.pyi": 1782063765067823837, + "venv/lib/python3.13/site-packages/_pytest/faulthandler.py": 13329320306353718649, + "venv/lib/python3.13/site-packages/numpy/__init__.cython-30.pxd": 1476688171642957054, + "venv/lib/python3.13/site-packages/pip/_internal/utils/deprecation.py": 7435269308988385470, + "venv/lib/python3.13/site-packages/smmap/test/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_smoke.py": 7476432112986643847, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_numba.py": 203614377324678234, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py": 17520043776344667309, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/hkdf.py": 8539232219382152238, + "venv/lib/python3.13/site-packages/pandas/core/indexes/extension.py": 7193879749426425951, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/discovery.py": 12479996594187326798, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/__init__.py": 13331622784664204066, + "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe.py": 16525846817331842080, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_array.py": 11271807719833350332, + "venv/lib/python3.13/site-packages/pydantic/experimental/arguments_schema.py": 8644805798814234457, + "venv/lib/python3.13/site-packages/fastapi/dependencies/utils.py": 7687214523082385505, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/getlimits.pyi": 93747984191461691, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2py2e.py": 8571347023940385644, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_assumed_shape.py": 18361009188431483164, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/writeonly.py": 14213960431308620600, + "venv/lib/python3.13/site-packages/pandas/api/typing/__init__.py": 6438268275857060114, + "venv/lib/python3.13/site-packages/pygments/styles/lightbulb.py": 12433916113773598760, + "venv/lib/python3.13/site-packages/pygments/lexers/whiley.py": 218468511185731869, + "venv/lib/python3.13/site-packages/numpy/_core/defchararray.pyi": 2277933368373498515, + "venv/lib/python3.13/site-packages/pandas/testing.py": 18352975429944961593, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply.py": 8103417303073393969, + "frontend/src/lib/stores/__tests__/test_sidebar.js": 113566022614194798, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_iteration.py": 2118501258411211851, + "frontend/src/lib/toasts.js": 199299975383904134, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_names.py": 10037208029090713121, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_object_arr.py": 4725156416161661681, + "frontend/build/_app/immutable/chunks/CTFFznDe.js": 8010860292122799172, + "frontend/src/lib/components/reports/__tests__/report_card.ux.test.js": 14899927226278257748, + "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py": 17043883737206361151, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/licenses/LICENSE": 11199044866758471950, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/core/indexes/interval.py": 17965911911189276907, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_setops.py": 16240008859650640717, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_nlargest.py": 1423106021399469694, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/__init__.py": 15130871412783076140, + "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js": 13047006106880121194, + "frontend/src/components/git/CommitModal.svelte": 4151596094672629211, + "venv/lib/python3.13/site-packages/pandas/tests/extension/list/__init__.py": 10072943986583771248, + "venv/lib/python3.13/site-packages/pillow.libs/libfreetype-ee1c40c4.so.6.20.4": 12301510769231120312, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.f": 1486878017876900890, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/common.py": 2075178842766962782, + "backend/git_repos/remote/test-repo.git/objects/22/c0ea49cea7e5138fb91b25b1bc0e101ae6e017": 17630871400599334193, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/utils.h": 3920393832993063886, + "backend/src/plugins/translate/superset_executor.py": 454191269003159946, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_clipboard.py": 12674078499544938252, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/warnings_and_errors.pyi": 9019577563960071498, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_cisco.py": 1786475632318043669, + "venv/lib/python3.13/site-packages/pygments/lexers/roboconf.py": 1335271183806115470, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunc_config.pyi": 2005481648919369399, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/einsumfunc.pyi": 8902765738896588686, + "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.py": 5570375156851166818, + "venv/lib/python3.13/site-packages/keyring/backends/macOS/__init__.py": 1010937986753403630, + "venv/lib/python3.13/site-packages/pandas/tests/test_col.py": 14781555956509956371, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_constructors.py": 2439700578107653984, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_analytics.py": 8155550493311083506, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_sequence.py": 3721630758772772345, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/crud.py": 805684028038896506, + "venv/lib/python3.13/site-packages/pandas/core/window/ewm.py": 12143982379243011357, + "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.py": 18402126171936308228, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_range.py": 9900908262700749861, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_missing.py": 3991199131191840903, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/__init__.py": 11414161642984633362, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_format.py": 1947585389944838076, + "venv/lib/python3.13/site-packages/pygments/lexers/felix.py": 2334148799642808111, + "venv/lib/python3.13/site-packages/pip/_internal/utils/entrypoints.py": 11268059714209889385, + "venv/lib/python3.13/site-packages/jeepney/io/__init__.py": 2568357146184259175, + "venv/lib/python3.13/site-packages/pip/_internal/commands/freeze.py": 8644678595442275485, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/spinner.py": 5564789578696016201, + "venv/lib/python3.13/site-packages/numpy/_core/_internal.py": 13056946834347643109, + "venv/lib/python3.13/site-packages/keyring/util/platform_.py": 16978095707950463638, + "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.py": 15031197044862908440, + "venv/lib/python3.13/site-packages/pandas/core/algorithms.py": 487939626203097713, + "venv/lib/python3.13/site-packages/smmap/test/test_tutorial.py": 16309370344201290582, + "venv/lib/python3.13/site-packages/pandas/tests/test_register_accessor.py": 16661086720613729974, + "venv/lib/python3.13/site-packages/pandas/tests/tools/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/tests/test_version.py": 6317733744899804292, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_read_fwf.py": 13684564756628524738, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_empty.py": 6845813091876023610, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/conftest.py": 17514079438846557943, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.py": 5221550291012182443, + "venv/lib/python3.13/site-packages/apscheduler/__init__.py": 8652514118183227412, + "venv/lib/python3.13/site-packages/numpy/_configtool.pyi": 9670892208858157218, + "venv/lib/python3.13/site-packages/PIL/WmfImagePlugin.py": 15685062825106444247, + "venv/lib/python3.13/site-packages/pygments/lexers/floscript.py": 944042220740663491, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/privatemod.f90": 8999550841368314739, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/pager.py": 18072372651585577904, + "venv/lib/python3.13/site-packages/requests/adapters.py": 17161109043243436200, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/rsa.py": 7852235015155694076, + "venv/bin/pytest": 2593802493707545818, + "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.cpython-313-x86_64-linux-gnu.so": 16327106317503267067, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py": 17786759398959482432, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reshaping.py": 18085648077811502689, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/syntax.py": 18070449217682341495, + "venv/lib/python3.13/site-packages/numpy/lib/mixins.pyi": 15597536290223628777, + "venv/lib/python3.13/site-packages/click/_winconsole.py": 8133229457213704188, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/cursor.py": 8074527784886981710, + "venv/lib/python3.13/site-packages/uvicorn/config.py": 17317823506849880916, + "venv/lib/python3.13/site-packages/rpds/rpds.cpython-313-x86_64-linux-gnu.so": 561326734302492859, + "frontend/src/routes/+layout.ts": 8392799000856778740, + "venv/lib/python3.13/site-packages/websockets/sync/utils.py": 15135020989207191168, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_builtin.py": 74545289993348593, + "venv/lib/python3.13/site-packages/pandas/compat/_optional.py": 12714208019736797073, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/meson.build": 17468163147765825687, + "venv/lib/python3.13/site-packages/pandas/core/reshape/pivot.py": 4736553530021653868, + "backend/src/core/scheduler.py.bak": 15045927989195339728, + "venv/lib/python3.13/site-packages/pydantic/v1/py.typed": 15130871412783076140, + "backend/tests/test_translate_executor_filter.py": 11717560360659156703, + "backend/git_repos/remote/test-repo.git/objects/17/66251d8cb2693136e09ea3def3e776559cb722": 1137762010493457272, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_engines.py": 2790877023773269276, + "venv/lib/python3.13/site-packages/sqlalchemy/util/topological.py": 11586868018611359415, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/test_pyinstaller.py": 9430762168079129025, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_defmatrix.py": 2982572083770917411, + "venv/lib/python3.13/site-packages/keyring/backends/fail.py": 5792496235026678021, + "venv/lib/python3.13/site-packages/fastapi/_compat/main.py": 13016492818231995760, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_index_equal.py": 10617870815989304510, + "venv/lib/python3.13/site-packages/authlib/oidc/core/models.py": 1370998944213578392, + "backend/src/core/utils/network.py": 2942301957401471733, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/interfaces.py": 4890958419958257958, + "venv/lib/python3.13/site-packages/PIL/TiffImagePlugin.py": 6845043550424565106, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hmac.py": 18132740541139891824, + "venv/lib/python3.13/site-packages/PIL/BdfFontFile.py": 10811640174253687845, + "venv/lib/python3.13/site-packages/git/objects/submodule/__init__.py": 12958523219496107878, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_astype.py": 4023134352880775277, + "backend/git_repos/remote/test-repo.git/objects/6a/f2b28e314c04fb4e03476c12d6491213591512": 4526311953587849385, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlrd.py": 9982457234958102003, + "venv/lib/python3.13/site-packages/fastapi/applications.py": 4975013401500183420, + "venv/lib/python3.13/site-packages/pandas/_config/display.py": 433650013567734821, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_utils.py": 15197339054699289627, + "venv/lib/python3.13/site-packages/pillow.libs/libtiff-295fd75c.so.6.2.0": 17866578820357536656, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/bar.py": 12859947592168238832, + "venv/lib/python3.13/site-packages/numpy/_typing/_add_docstring.py": 14657346268890680694, + "venv/lib/python3.13/site-packages/numpy/_typing/_nested_sequence.py": 7827259347937485408, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_quarter.py": 6795026338946382805, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/RECORD": 14375680579341422646, + "venv/lib/python3.13/site-packages/pip/_internal/utils/appdirs.py": 539688259643447487, + "venv/lib/python3.13/site-packages/pygments/styles/lilypond.py": 12486578704132023691, + "venv/lib/python3.13/site-packages/_pytest/stash.py": 14430252294763173610, + "venv/lib/python3.13/site-packages/gitdb/stream.py": 3035738824550231197, + "venv/lib/python3.13/site-packages/gitdb/util.py": 645937410168583692, + "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.py": 546976622482819095, + "venv/lib/python3.13/site-packages/apscheduler/events.py": 10748346950729364899, + "venv/lib/python3.13/site-packages/passlib/tests/test_hosts.py": 8284535274820610802, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.pyi": 1830368009811266890, + "venv/lib/python3.13/site-packages/cryptography/x509/general_name.py": 4481582226129488535, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/__init__.py": 15130871412783076140, + "backend/src/api/auth.py": 13309519397410879911, + "venv/lib/python3.13/site-packages/jeepney/wrappers.py": 15234251727654692435, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/cymysql.py": 5570881524647789741, + "backend/src/api/routes/git/__init__.py": 14824479456728364642, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/random.pyi": 1808392097712315455, + "venv/lib/python3.13/site-packages/pip/_internal/__init__.py": 7060345458485901709, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py": 5450931608244830335, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE": 14610443802372107702, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/RECORD": 3288669175908898843, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reorder_levels.py": 10996225584998355824, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_index_as_string.py": 13421551616136437100, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py": 10843527833285941674, + "backend/src/api/routes/git/_environment_routes.py": 1034235394475003092, + "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.pyi": 2667658052734929089, + "venv/lib/python3.13/site-packages/pydantic/_internal/_utils.py": 10936299012115637790, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_base.py": 15292769510056833117, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_arithmetic.py": 4858019700859362660, + "frontend/e2e/e2e-nohealth.mjs": 5921562935931064381, + "backend/src/models/storage.py": 1218824901001897736, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_loop.py": 6502725909989655118, + "backend/src/plugins/translate/dictionary_correction.py": 18051915404774774954, + "backend/src/core/logger/__tests__/test_logger.py": 4188397720199242180, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/events.py": 7576713358952064464, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/ssh.py": 16262079536832961001, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/response.py": 353446217435156470, + "venv/lib/python3.13/site-packages/pygments/styles/material.py": 14325514369696961874, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/provision.py": 7362363963464621570, + "venv/lib/python3.13/site-packages/rapidfuzz/__init__.pyi": 13131938031654453415, + "venv/lib/python3.13/site-packages/passlib/__init__.py": 6987142392711441525, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_utils.py": 4612923732868929770, + "backend/src/api/routes/assistant/_dispatch.py": 3811422882986895565, + "backend/src/services/dataset_review/orchestrator_pkg/_commands.py": 15474996500928063708, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py": 7301817934902711510, + "venv/lib/python3.13/site-packages/ecdsa/numbertheory.py": 13538480862642502330, + "venv/lib/python3.13/site-packages/uvicorn/main.py": 5843333502356171120, + "venv/lib/python3.13/site-packages/websockets/asyncio/__init__.py": 15130871412783076140, + "frontend/src/lib/components/dataset-review/LaunchConfirmationPanel.svelte": 14399834101283315718, + "venv/lib/python3.13/site-packages/yaml/__init__.py": 11044333164208875121, + "venv/lib/python3.13/site-packages/passlib/tests/sample_config_1s.cfg": 1809713544666311828, + "venv/lib/python3.13/site-packages/pandas/core/window/doc.py": 4204367435727844696, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_utils.py": 8668650648802195016, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_promote.py": 16601872688404846307, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/METADATA": 3643456577199395707, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/METADATA": 10241403930811743893, + "venv/lib/python3.13/site-packages/numpy/testing/overrides.pyi": 17408923209056015964, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rank.py": 14115215895836854973, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_interp_fillna.py": 5530041283991655415, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/baked.py": 6911138208603240867, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/licenses/LICENSE": 3509878235770224233, + "frontend/src/routes/datasets/+page.svelte": 1253723176637759895, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_api.py": 6439927798101672423, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_comment.py": 5603075924832933306, + "venv/lib/python3.13/site-packages/requests/cookies.py": 4997226307650350876, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_constructors.py": 6515602441449955627, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.py": 3762934316884129514, + "venv/lib/python3.13/site-packages/apscheduler/util.py": 11440701924954413440, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8693/__init__.py": 11065844267208884531, + "venv/lib/python3.13/site-packages/pygments/lexers/hdl.py": 6772823608124143123, + "venv/bin/pyrsa-priv2pub": 16766314338551408621, + "backend/git_repos/remote/test-repo.git/config": 14478893374498894766, + "venv/lib/python3.13/site-packages/pip/_internal/commands/check.py": 6937252393012787214, + "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.py": 15989335816452913951, + "venv/lib/python3.13/site-packages/_pytest/config/__init__.py": 12787579251203510616, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/linalg.pyi": 7217817172953767893, + "venv/lib/python3.13/site-packages/typing_inspection/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/computation/expr.py": 13085709365673019494, + "backend/src/plugins/translate/service_utils.py": 15103456860964805370, + "venv/lib/python3.13/site-packages/pandas/core/tools/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py": 16550134594771291234, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_misc.py": 9190561773853901527, + "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.py": 3646474165295296669, + "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.py": 6348811074249398741, + "venv/lib/python3.13/site-packages/gitdb/test/test_stream.py": 18159319442241857857, + "venv/lib/python3.13/site-packages/numpy/typing/__init__.pyi": 10058149460938883683, + "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/__init__.py": 1229691061011958769, + "venv/lib/python3.13/site-packages/numpy/__init__.pxd": 13527602429695748857, + "venv/lib/python3.13/site-packages/passlib/crypto/__init__.py": 16409196868630049365, + "venv/lib/python3.13/site-packages/pip/_internal/commands/show.py": 11839987586497822300, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets_properties.py": 2340780879864207500, + "venv/lib/python3.13/site-packages/pandas/io/formats/xml.py": 12235904458551067063, + "venv/lib/python3.13/site-packages/cffi/recompiler.py": 17809252277700094304, + "venv/lib/python3.13/site-packages/pluggy/_warnings.py": 2664977718908223842, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.pyi": 2700568594438441246, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_numba.py": 1548992314517942530, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py": 13215932786801020434, + "venv/lib/python3.13/site-packages/pillow.libs/libwebp-d8b9687f.so.7.2.0": 116215608212382352, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_api.py": 13739775047493489785, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/__init__.py": 8924316361489544973, + "venv/lib/python3.13/site-packages/numpy/_core/tests/_natype.py": 12013743717306900860, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/__init__.py": 2397613800206300456, + "venv/lib/python3.13/site-packages/PIL/_imagingcms.cpython-313-x86_64-linux-gnu.so": 15198265718775753589, + "venv/lib/python3.13/site-packages/pandas/io/formats/html.py": 11849941990375030601, + "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.cpython-313-x86_64-linux-gnu.so": 9380059465932560412, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_ticks.py": 11649965080303468085, + "venv/lib/python3.13/site-packages/pygments/lexers/textedit.py": 14872291021083255414, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/licenses/LICENSE": 14230156892574098522, + "venv/lib/python3.13/site-packages/numpy/f2py/setup.cfg": 17974180080760231128, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_indexing.py": 1926721846569184112, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/_json.py": 6718137926109998531, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/intranges.py": 12536174834761591006, + "frontend/static/favicon.svg": 6451919037497541980, + "venv/lib/python3.13/site-packages/starlette/middleware/__init__.py": 9524277524025372856, + "venv/lib/python3.13/site-packages/fastapi/_compat/v1.py": 9776181748061371513, + "venv/lib/python3.13/site-packages/click/utils.py": 13475604420010022458, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_cpp.cpython-313-x86_64-linux-gnu.so": 7490281502544276964, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/RECORD": 11976403473353722969, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_series.pyi": 7112486730197067087, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_xs.py": 1897144171324167738, + "venv/lib/python3.13/site-packages/rsa/transform.py": 12261738253972114335, + "venv/lib/python3.13/site-packages/numpy/_core/umath.pyi": 11775804030935134661, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_localize.py": 14495969595744172955, + "docker/nginx.conf": 14595959129793115016, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nunique.py": 8450574751331186943, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py": 14768815444701392013, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/authorization_server.py": 2913800420669417152, + "venv/lib/python3.13/site-packages/pandas/_testing/_hypothesis.py": 12298650110193262367, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/schema.py": 5391655516870611151, + "venv/lib/python3.13/site-packages/git/db.py": 6680123567509774609, + "frontend/e2e/fixtures/global-setup.js": 1597253740213759490, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/libdivide.h": 5314232500579481313, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/__init__.py": 8957417237987377675, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/RECORD": 5612689057373270213, + "venv/lib/python3.13/site-packages/yaml/scanner.py": 8554164423030397366, + "frontend/src/routes/dashboards/+page.svelte": 12050524336689510248, + "frontend/src/lib/api/datasetReview.js": 3023829599261929428, + "venv/lib/python3.13/site-packages/numpy/core/_dtype.pyi": 15130871412783076140, + "backend/src/scripts/test_dataset_dashboard_relations.py": 6373309008834670516, + "venv/lib/python3.13/site-packages/passlib/handlers/md5_crypt.py": 7669328623914474137, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_amd64_unix.h": 16679226724722970205, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/parameter.py": 2793853093222900337, + "venv/lib/python3.13/site-packages/tzlocal/utils.py": 7893867974548929154, + "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.pyi": 11535882608355652318, + "backend/git_repos/remote/test-repo.git/refs/heads/dev": 8596267258668462395, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py": 5324490103277723813, + "venv/lib/python3.13/site-packages/sqlalchemy/event/__init__.py": 16182540084654095394, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/styled.py": 8726407298858811635, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.pyi": 3783371827926657291, + "frontend/build/_app/immutable/nodes/24.CSPGrWA3.js": 869128617737549280, + "backend/src/services/clean_release/repositories/policy_repository.py": 10990781278715170404, + "venv/lib/python3.13/site-packages/pandas/_libs/indexing.pyi": 90995502095752726, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_base.py": 16710163879060522196, + "venv/lib/python3.13/site-packages/httpcore/__init__.py": 5897259569277532401, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/WHEEL": 7314207500206292683, + "frontend/src/lib/stores/__tests__/sidebar.test.js": 5168138864675110669, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/env.py": 1254612137164491329, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timezones.py": 1962830142478494009, + "frontend/postcss.config.js": 5714274776976071678, + "venv/lib/python3.13/site-packages/pandas/tests/extension/list/test_list.py": 4538625980063375611, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/names.py": 705261158040926128, + "venv/lib/python3.13/site-packages/pip/_internal/configuration.py": 2563100675379605810, + "venv/lib/python3.13/site-packages/numpy/__init__.py": 2331213407497901634, + "venv/lib/python3.13/site-packages/git/compat.py": 16843345645628515492, + "frontend/build/_app/immutable/nodes/25.Diztcgpz.js": 6833313483301491246, + "venv/lib/python3.13/site-packages/authlib/jose/jwk.py": 13560749074922587626, + "backend/src/plugins/translate/dictionary_import_export.py": 12639118203353044308, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqldb.py": 2138006249556140141, + "backend/src/core/superset_client/_datasets_preview.py": 16476837380317508068, + "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.pyi": 3111521453255595221, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arithmetic.pyi": 467927803068643024, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi": 10784666243744187693, + "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.pyi": 1942236362568669510, + "venv/lib/python3.13/site-packages/numpy/lib/introspect.py": 5706729538356302335, + "venv/lib/python3.13/site-packages/pygments/lexers/arrow.py": 6699009974224818979, + "backend/src/api/routes/dashboards/_schemas.py": 10686942001147126057, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__main__.py": 111505188798650285, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/RECORD": 16740224557818847141, + "venv/lib/python3.13/site-packages/pyasn1/type/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/RECORD": 17594742868855466393, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nlargest_nsmallest.py": 11049656379959696695, + "venv/lib/python3.13/site-packages/pydantic_settings/version.py": 17263591404988984480, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_compat.py": 11481259393140789881, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/RECORD": 17341460504914308184, + "backend/src/services/clean_release/stages/no_external_endpoints.py": 11899278509901673808, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/mod_derived_types.f90": 2556756676983488922, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-2.csv": 16611724551361692999, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/METADATA": 13695483537390128881, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_append.py": 9644955836191900614, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/sqltypes.py": 2952996080192295188, + "venv/lib/python3.13/site-packages/pyasn1/codec/cer/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/pygments/lexers/numbair.py": 17382646010399794697, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/RECORD": 8201759541483070364, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayprint.py": 5912583751693302370, + "venv/lib/python3.13/site-packages/numpy/core/_internal.py": 14816989375912210674, + "venv/lib/python3.13/site-packages/certifi/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.f": 18215492757907465667, + "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.cpython-313-x86_64-linux-gnu.so": 3478629877770507853, + "venv/lib/python3.13/site-packages/pandas/core/indexers/objects.py": 409599993084996593, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/enumerated.py": 16623643153933348755, + "venv/lib/python3.13/site-packages/urllib3/util/wait.py": 14629837381061032546, + "venv/lib/python3.13/site-packages/pygments/lexers/_postgres_builtins.py": 2383906454995727700, + "venv/lib/python3.13/site-packages/websockets/frames.py": 4901850477414613953, + "backend/src/services/__tests__/test_encryption_manager.py": 7097521778855634174, + "backend/src/models/dataset_review_pkg/_filter_models.py": 8426742165406106645, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_cumulative.py": 16283954014170928273, + "venv/lib/python3.13/site-packages/httpx/_status_codes.py": 1758440600462491839, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/util/__init__.py": 15821192733097665649, + "frontend/build/_app/immutable/chunks/BsdYA8vJ.js": 17313445894049649179, + "backend/src/services/clean_release/repositories/artifact_repository.py": 9040122866024534865, + "venv/lib/python3.13/site-packages/starlette/staticfiles.py": 11256974306202558529, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraysetops.pyi": 1782641795267171928, + "venv/lib/python3.13/site-packages/pygments/lexers/_lasso_builtins.py": 4006621424665751347, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numba.py": 18148155441554555618, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/licenses/COPYING": 14459782230785388765, + "backend/src/api/routes/__tests__/test_reports_detail_api.py": 7284151749869192310, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/timeseries.py": 17641722582848932332, + "venv/lib/python3.13/site-packages/jsonschema/_types.py": 9250091433305395923, + "venv/lib/python3.13/site-packages/greenlet/TThreadStateDestroy.cpp": 13749797532421836328, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.cpython-313-x86_64-linux-gnu.so": 12776951671822017500, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jws_algs.py": 11920621906391796226, + "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/styles/onedark.py": 17580335614588531275, + "venv/lib/python3.13/site-packages/pip/_internal/cli/base_command.py": 10660882478126415399, + "venv/lib/python3.13/site-packages/pygments/styles/lovelace.py": 15082819162722685616, + "venv/lib/python3.13/site-packages/pygments/lexers/templates.py": 16267123844830647955, + "frontend/src/routes/dashboards/health/+page.svelte": 5234355531444674267, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/token_validator.py": 13917237710752731397, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/models.py": 3567900447243180991, + "backend/git_repos/remote/test-repo.git/hooks/post-update.sample": 4288868784459664203, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/asyncio.py": 11442130378027628520, + "venv/lib/python3.13/site-packages/numpy/ma/LICENSE": 6723325434476453127, + "venv/lib/python3.13/site-packages/pillow.libs/liblcms2-cc10e42f.so.2.0.17": 4221600618602131476, + "venv/lib/python3.13/site-packages/passlib/handlers/django.py": 66638338316838351, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_where.py": 16584772339591698754, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers.py": 4940305787631493030, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_reductions.py": 17810094294132819208, + "frontend/build/_app/immutable/chunks/CR_piKg4.js": 4315960667699052933, + "venv/lib/python3.13/site-packages/pandas/core/_numba/__init__.py": 15130871412783076140, + "backend/src/plugins/translate/_batch_proc.py": 9916377448262126536, + "venv/lib/python3.13/site-packages/jose/backends/base.py": 12795468809472351388, + "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_records.py": 12217508428903265797, + "venv/lib/python3.13/site-packages/pygments/styles/algol_nu.py": 6229131016121450375, + "venv/lib/python3.13/site-packages/numpy/_typing/_nbit.py": 15498810174380798405, + "venv/lib/python3.13/site-packages/pandas/core/computation/engines.py": 1889823242704769952, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_finfo.py": 17174674209412768422, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/__init__.py": 11121941644106061453, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filters/__init__.py": 835924536855006596, + "venv/lib/python3.13/site-packages/referencing/_attrs.py": 14434891919747863272, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_count.py": 16947194157984708529, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_getitem.py": 11882213323878588707, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/token.py": 11655733747403312358, + "venv/lib/python3.13/site-packages/PIL/PsdImagePlugin.py": 4347581352716313298, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/WHEEL": 13232065379159720345, + "venv/lib/python3.13/site-packages/greenlet/greenlet_slp_switch.hpp": 1694714764745718414, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/util.py": 9373891872370603585, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_shift.py": 14026124784027171225, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/METADATA": 3448868793744358515, + "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/licenses/LICENSE": 5757922580822401219, + "venv/lib/python3.13/site-packages/apscheduler/executors/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/reshape/melt.py": 14109947554124551986, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_pydatetime.py": 16228315407662560992, + "venv/lib/python3.13/site-packages/fastapi/openapi/utils.py": 9058442333718187759, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg8000.py": 6580512354643849828, + "venv/lib/python3.13/site-packages/PIL/_version.py": 3025206805667405876, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_explode.py": 16195468192763502449, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_repeat.py": 18394463352314692642, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_kwargs.py": 10173906126211473463, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/char.pyi": 13143560686049627445, + "venv/lib/python3.13/site-packages/PIL/_typing.py": 17407980360703360848, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/licenses/LICENSE": 11986475601091748012, + "venv/lib/python3.13/site-packages/pygments/lexers/html.py": 17288963642862550116, + "backend/src/services/reports/type_profiles.py": 8365014439236739390, + "frontend/build/_app/immutable/chunks/DXB0WcN8.js": 6620985551681767720, + "backend/src/plugins/translate/orchestrator_validation.py": 17464651621865127561, + "venv/lib/python3.13/site-packages/httpcore/_async/http11.py": 7565741048880549265, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.pyi": 2700568594438441246, + "backend/src/core/utils/superset_context_extractor/_filters.py": 281666313889333545, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_duplicated.py": 14676058112867601208, + "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.pyi": 17825266810219495672, + "venv/lib/python3.13/site-packages/PIL/PixarImagePlugin.py": 12061717157676819554, + "venv/lib/python3.13/site-packages/numpy/_core/_asarray.pyi": 3940937389249665490, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/__init__.py": 6654484356035994666, + "merge_kilo.py": 4282227344165325430, + "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.pyi": 2628035888703031029, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/AUTHORS": 17022185907188244883, + "venv/lib/python3.13/site-packages/pydantic/schema.py": 83238111330132033, + "backend/tests/core/migration/test_dry_run_orchestrator.py": 5158126098685923841, + "venv/lib/python3.13/site-packages/pygments/lexers/wgsl.py": 9718384325049172855, + "venv/lib/python3.13/site-packages/numpy/_core/function_base.py": 5676611841071163854, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/assignOnlyModule.f90": 15967199601254912096, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_partial.py": 15129217814262096441, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/_mapping.py": 17519715342700184302, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ctypeslib.pyi": 8623725566434725526, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/RECORD": 963865046004908868, + "venv/lib/python3.13/site-packages/pydantic/v1/parse.py": 5875950480538559709, + "frontend/build/_app/immutable/nodes/15.DvrdX5FQ.js": 7879930226751605252, + "venv/lib/python3.13/site-packages/requests/auth.py": 912521029864094489, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_arithmetic.py": 976970179757115407, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py": 3515239440812977025, + "venv/lib/python3.13/site-packages/numpy/lib/mixins.py": 7404729635424818497, + "venv/lib/python3.13/site-packages/requests/__init__.py": 2604491098990438229, + "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.pyi": 362674842164060822, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_mixins.py": 14293848457775268010, + "frontend/build/_app/immutable/chunks/DnU4uHLm.js": 18020186069872902477, + "backend/src/plugins/translate/_token_budget.py": 102333806375699922, + "venv/lib/python3.13/site-packages/passlib/utils/decor.py": 21351391767102899, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/wrappers.py": 315409799569602977, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py": 16463243654123731993, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/json.py": 6429651405841289922, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x448.pyi": 11733449165276720901, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_pickle.py": 4216445955812104014, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_integer.f90": 8308927438994838783, + "venv/lib/python3.13/site-packages/pygments/lexers/ambient.py": 18106929781201758080, + "backend/git_repos/remote/test-repo.git/objects/aa/ea042a37c6f7bd010d91db93951328da7f01cc": 11936716919619613939, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.cpython-313-x86_64-linux-gnu.so": 6726636942028566415, + "venv/lib/python3.13/site-packages/pandas/core/ops/invalid.py": 10186546927135277579, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_cumulative.py": 11752712818547022872, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_constructors.py": 3440572832185940666, + "venv/lib/python3.13/site-packages/pandas/io/excel/__init__.py": 6662687963241471393, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/__init__.py": 4752355175318979033, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/__init__.py": 9106887608906281686, + "venv/lib/python3.13/site-packages/keyring/backends/macOS/api.py": 4502902558534812719, + "venv/lib/python3.13/site-packages/greenlet/tests/test_generator_nested.py": 12822240347707346718, + "venv/lib/python3.13/site-packages/pygments/lexers/ada.py": 17032408976634308999, + "frontend/src/lib/stores/__tests__/setupTests.js": 13316015450930652502, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_tools.py": 13994164937603424236, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_from_scalar.py": 16520136036631155842, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/licenses/LICENSE": 1126906383149772681, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA": 7788873460115135563, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/endpoints.py": 18334891787808122725, + "frontend/src/routes/tools/mapper/+page.svelte": 11954517841045690204, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_asof.py": 15792528637923603240, + "backend/git_repos/remote/test-repo.git/objects/22/91d2eb86d77accf7401f8952778809cd1aa4d4": 5120428041118096941, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/setup.py": 5482352991573617756, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_constructors.py": 11786151805350138436, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_blocking.py": 6312017389529916004, + "venv/lib/python3.13/site-packages/numpy/random/_generator.cpython-313-x86_64-linux-gnu.so": 15625019745021657670, + "venv/lib/python3.13/site-packages/pandas/core/arrays/numpy_.py": 16641729409992576358, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/session.py": 5126234270435358377, + "venv/lib/python3.13/site-packages/_pytest/capture.py": 17850967053989495422, + "venv/lib/python3.13/site-packages/dateutil/tz/tz.py": 5258008267965780817, + "venv/lib/python3.13/site-packages/pygments/lexers/json5.py": 9736695186548899403, + "venv/lib/python3.13/site-packages/fastapi/responses.py": 6092572095549573593, + "venv/lib/python3.13/site-packages/multipart/exceptions.py": 8580522204565816132, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi": 11455144434558323236, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/endpoint.py": 8519226897299461194, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/WHEEL": 16784970174376303810, + "venv/lib/python3.13/site-packages/pygments/lexers/devicetree.py": 9820277168995231927, + "backend/src/services/llm_prompt_templates.py": 5706065356933042518, + "backend/alembic/README": 17625003422504608299, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/_arrow_utils.py": 17808805047762566525, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/provision.py": 13156893203444936309, + "venv/lib/python3.13/site-packages/PIL/PdfImagePlugin.py": 16082854250442815862, + "venv/lib/python3.13/site-packages/pygments/lexers/srcinfo.py": 16466766917840327610, + "backend/src/plugins/migration.py": 16567640288127443175, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nditer.pyi": 33523064068376750, + "venv/lib/python3.13/site-packages/pygments/lexers/_qlik_builtins.py": 1728058904137960444, + "backend/git_repos/remote/test-repo.git/objects/71/47ad0836409b35c1650dddf5fa6329ba61a824": 16601919569600972016, + "venv/lib/python3.13/site-packages/psycopg2/errorcodes.py": 6711830404954314463, + "venv/lib/python3.13/site-packages/_pytest/assertion/truncate.py": 14000392306994773741, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_contains.py": 17630310725250553247, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_getitem.py": 8414677531950073373, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_indexing.py": 6539365174681852769, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py": 9888795404090331319, + "venv/lib/python3.13/site-packages/pygments/lexers/d.py": 18120801227801223376, + "venv/lib/python3.13/site-packages/dateutil/zoneinfo/__init__.py": 10261654903106416888, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_nat.py": 7400568652984772972, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/groupby.py": 12749730954888633240, + "venv/lib/python3.13/site-packages/pygments/lexers/c_like.py": 13004061780700827047, + "venv/lib/python3.13/site-packages/numpy/f2py/__main__.py": 12567751646090102167, + "venv/lib/python3.13/site-packages/keyring/util/__init__.py": 9635016046220094473, + "venv/lib/python3.13/site-packages/websockets/legacy/server.py": 5390090919193784186, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/base.cpython-313-x86_64-linux-gnu.so": 8335798324308957127, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_arithmetic.py": 13304014482504911016, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.pyi": 13068936038543812082, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_categorical.py": 8844412630511751140, + "venv/lib/python3.13/site-packages/pandas/io/parsers/base_parser.py": 15148258424429418380, + "frontend/src/lib/api/translate/datasources.js": 17202165717960970061, + "frontend/vitest.config.js": 142965001777137074, + "venv/lib/python3.13/site-packages/pandas/core/ops/common.py": 17314999825118928100, + "venv/lib/python3.13/site-packages/python_multipart/__init__.py": 10290895261363712562, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/scalars.pyi": 3130685119595923378, + "venv/lib/python3.13/site-packages/pandas/_libs/arrays.cpython-313-x86_64-linux-gnu.so": 8634234308863090562, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_base_indexer.py": 3129123098288498633, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_ufunclike.py": 13752381070039804384, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_freq_attr.py": 17290447066681837965, + "venv/lib/python3.13/site-packages/greenlet/tests/test_stack_saved.py": 7767924647054186572, + "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.py": 11758491058846790938, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_multi.py": 4612816468258071810, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/exceptions.py": 11303331612406662162, + "venv/lib/python3.13/site-packages/ecdsa/test_ellipticcurve.py": 11125783981574901613, + "venv/lib/python3.13/site-packages/fastapi/_compat/shared.py": 5249003014594464718, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.pyi": 3595910367556664837, + "venv/lib/python3.13/site-packages/greenlet/tests/test_throw.py": 4119265751381210399, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_add_prefix_suffix.py": 4815518357936092237, + "venv/lib/python3.13/site-packages/PIL/_avif.pyi": 18222325750818585549, + "venv/lib/python3.13/site-packages/pygments/styles/igor.py": 2291640843781128270, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.obj": 11967637259540485652, + "venv/lib/python3.13/site-packages/greenlet/platform/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/attr/_compat.py": 4016698632899969390, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_pickle.py": 3419420815386925630, + "frontend/src/lib/components/reports/ReportDetailPanel.svelte": 16014663571033060148, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_index_as_string.py": 292438690259156869, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_cell_widths.py": 5724379364993806351, + "venv/lib/python3.13/site-packages/starlette/requests.py": 4582424111350387010, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE": 2345070715845618109, + "venv/lib/python3.13/site-packages/pandas/io/pytables.py": 17482934215513855928, + "venv/lib/python3.13/site-packages/jeepney/io/tests/utils.py": 10346859893683522113, + "venv/lib/python3.13/site-packages/pygments/lexers/ecl.py": 12886695201994337568, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh15035.f": 13126319439858352711, + "venv/lib/python3.13/site-packages/pygments/lexers/perl.py": 17014528337020572681, + "frontend/src/components/auth/ProtectedRoute.svelte": 7705373237339863677, + "frontend/src/components/tasks/TaskLogPanel.svelte": 4685456099513280294, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append.py": 13152738905565588661, + "backend/src/schemas/health.py": 13764741380188041840, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/testing.pyi": 9317922890783549326, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_stride_tricks.py": 1064774375151677743, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_object.py": 9215781825358309147, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/orderinglist.py": 10162099108656794338, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine_first.py": 11425059090050867171, + "backend/src/plugins/translate/events.py": 14264965160929343917, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/version.py": 5000350193835202949, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/RECORD": 10036839994791510159, + "venv/lib/python3.13/site-packages/websockets/utils.py": 17624459810483092475, + "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_reductions.py": 11624188672428941494, + "venv/lib/python3.13/site-packages/authlib/oauth2/base.py": 12948318231066662045, + "venv/lib/python3.13/site-packages/PIL/GribStubImagePlugin.py": 14954946414134984896, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/type_check.pyi": 1599910480066368464, + "venv/lib/python3.13/site-packages/uvicorn/__main__.py": 6086938803696646644, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/exc.py": 3242780937124999391, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_map.py": 32086858815359344, + "venv/lib/python3.13/site-packages/pandas/tests/base/__init__.py": 15130871412783076140, + "backend/src/core/superset_client/_charts.py": 8101226087578568224, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/__init__.py": 2345997696115514124, + "backend/src/schemas/auth.py": 12332369795585412464, + "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.py": 16539612003028676816, + "venv/lib/python3.13/site-packages/anyio/abc/_sockets.py": 15042495744291042062, + "venv/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py": 8145740626773638218, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_describe.py": 362374422228593281, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayterator.pyi": 15722265241660279277, + "venv/lib/python3.13/site-packages/pandas/core/sparse/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timedeltas.py": 1175883320586860493, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/WHEEL": 16223367184831500348, + "venv/lib/python3.13/site-packages/pillow.libs/libbrotlidec-b57ddf63.so.1.2.0": 10032590058372320813, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.pyx": 7085380878820747579, + "venv/lib/python3.13/site-packages/numpy/lib/array_utils.py": 18135114588643176359, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/quoted_character/foo.f": 15318083678609435813, + "venv/lib/python3.13/site-packages/pycparser/ply/__init__.py": 5040371336745852622, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_openid.py": 5670082466989878756, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/util.py": 3591114320020964752, + "backend/tests/scripts/test_clean_release_cli.py": 13978106497618764226, + "backend/tests/test_log_persistence.py": 11073909507089982937, + "venv/lib/python3.13/site-packages/idna/idnadata.py": 200840860629525970, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/socks.py": 17581293673028078134, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/missing.py": 14589973874359353799, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py": 7107619871245389835, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py": 1131520422778420758, + "venv/lib/python3.13/site-packages/_pytest/freeze_support.py": 494952714451267295, + "venv/lib/python3.13/site-packages/passlib/handlers/argon2.py": 835074952952017434, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/annotation.py": 15855576199737688124, + "venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py": 11015954144633661960, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/flatiter.pyi": 9598576095384661517, + "venv/lib/python3.13/site-packages/pillow.libs/libzstd-761a17b6.so.1.5.7": 17875236748140157032, + "venv/lib/python3.13/site-packages/authlib/integrations/django_client/__init__.py": 14186136918454802843, + "venv/lib/python3.13/site-packages/pandas/core/arrays/string_.py": 14865733321317939478, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/RECORD": 7363618870628306370, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/_io/saferepr.py": 8025234471485188730, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_join.py": 13115782559805895662, + "venv/lib/python3.13/site-packages/pygments/formatters/__init__.py": 8009871891954370829, + "venv/lib/python3.13/site-packages/jeepney/fds.py": 16573439672758641560, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/WHEEL": 17410883677306885019, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/applicator": 6616723520993329538, + "venv/lib/python3.13/site-packages/_pytest/recwarn.py": 15312759837838083596, + "venv/lib/python3.13/site-packages/pydantic/_internal/_signature.py": 17075003201648027250, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.pyi": 13208953770241664384, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/dml.py": 17308908394336670576, + "venv/lib/python3.13/site-packages/pandas/io/clipboard/__init__.py": 3656451154829206939, + "venv/lib/python3.13/site-packages/pygments/styles/gruvbox.py": 9489017112545247388, + "venv/lib/python3.13/site-packages/pygments/formatter.py": 11156733836937309465, + "venv/lib/python3.13/site-packages/apscheduler/triggers/combining.py": 1264860868414723829, + "venv/lib/python3.13/site-packages/pydantic/v1/generics.py": 1150594223414883339, + "venv/lib/python3.13/site-packages/anyio/to_thread.py": 12767326212338028587, + "venv/lib/python3.13/site-packages/numpy/random/__init__.pyi": 13833571330152615376, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/RECORD": 18294169019244826301, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo77.f": 4046503346951676582, + "venv/lib/python3.13/site-packages/pandas/util/_exceptions.py": 14426860079366867683, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/__init__.py": 17630019785411546070, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_setops.py": 13440916133402971190, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/RECORD": 4857922668040624663, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_factorize.py": 861897342553280506, + "venv/lib/python3.13/site-packages/pandas/core/groupby/groupby.py": 5536922766239875007, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/__init__.py": 3247870839323520754, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/status.py": 2214590120238144582, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parse_iso8601.py": 9100947620267156161, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/other.py": 6893672181003034910, + "venv/lib/python3.13/site-packages/pygments/lexers/_ada_builtins.py": 16331776886501464264, + "frontend/src/lib/components/health/HealthMatrix.svelte": 13165441444996153191, + "backend/src/api/routes/plugins.py": 4567336078922104278, + "venv/lib/python3.13/site-packages/pygments/lexers/varnish.py": 3608370156692616959, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nditer.pyi": 2351219156711800159, + "frontend/src/routes/migration/+page.svelte": 7821836225621176583, + "venv/lib/python3.13/site-packages/jose/backends/__init__.py": 16581763658239070112, + "backend/alembic/env.py": 14677445323675634060, + "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.py": 16856718240972376883, + "scripts/gen_semantics.py": 11397714264377363083, + "venv/lib/python3.13/site-packages/cryptography/__about__.py": 10471312538528457144, + "venv/lib/python3.13/site-packages/pygments/lexers/iolang.py": 10997512999728724130, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/specifiers.py": 12284901056355482991, + "venv/lib/python3.13/site-packages/pygments/lexers/gdscript.py": 12775803139854052838, + "venv/lib/python3.13/site-packages/greenlet/greenlet.cpp": 3281107194043468270, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/integration.py": 12670848015915724967, + "frontend/src/services/__tests__/gitService.test.js": 12845562082447336909, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_dst.py": 12243642467695371088, + "venv/lib/python3.13/site-packages/PIL/XpmImagePlugin.py": 6644626557482464312, + "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.py": 10803011906582324302, + "venv/bin/pyrsa-sign": 15564653706771700451, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/licenses/LICENSE": 16147999986886816451, + "venv/bin/uvicorn": 3776670267111625708, + "backend/src/models/config.py": 7631082065091961320, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_constructors.py": 12628643543371952431, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/base.py": 13269563625127294513, + "venv/lib/python3.13/site-packages/pydantic/v1/utils.py": 902648247561165429, + "venv/lib/python3.13/site-packages/_pytest/assertion/util.py": 16957445183087315124, + "venv/lib/python3.13/site-packages/sqlalchemy/events.py": 9329355486544822483, + "venv/lib/python3.13/site-packages/PIL/features.py": 3481835375516797487, + "frontend/src/components/git/GitInitPanel.svelte": 3002620133377315800, + "backend/src/api/routes/translate/_run_routes.py": 6617818713940695689, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/status_codes.py": 17005213506461710202, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_libalgos.py": 13934861984871432400, + "venv/lib/python3.13/site-packages/numpy/tests/test_ctypeslib.py": 9592016432737074765, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/live_render.py": 14619617939683800204, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/LICENSE": 16510279186416777136, + "venv/lib/python3.13/site-packages/PIL/IcoImagePlugin.py": 14483964191652228364, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/models.py": 5656065513234782067, + "venv/lib/python3.13/site-packages/httpcore/_sync/http_proxy.py": 14037211594707072958, + "venv/lib/python3.13/site-packages/urllib3/py.typed": 12220177918227495093, + "backend/src/api/routes/__tests__/test_connections_routes.py": 18044815559820610056, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.pyi": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_subclass.py": 13490541871827928531, + "venv/lib/python3.13/site-packages/PIL/_imagingmath.pyi": 18222325750818585549, + "venv/lib/python3.13/site-packages/pandas/tests/io/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/indexes/range.py": 3838277376774246164, + "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.pyi": 8607205990821121708, + "venv/lib/python3.13/site-packages/numpy/_core/_operand_flag_tests.cpython-313-x86_64-linux-gnu.so": 12296711931541109624, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh2848.f90": 5799106109999071412, + "venv/lib/python3.13/site-packages/pandas/core/reshape/tile.py": 17137524194129840833, + "venv/lib/python3.13/site-packages/pip/_internal/utils/datetime.py": 6816292030061301011, + "venv/lib/python3.13/site-packages/pandas/core/reshape/encoding.py": 12702053687535473061, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.pyi": 3573629536236108391, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_file.py": 5180401483285462901, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/pandas/_version.py": 3480474785419232091, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/markup.py": 5413800947725916538, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_fsspec.py": 6936167344144998172, + "venv/lib/python3.13/site-packages/websockets/uri.py": 264376168984862773, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_index.py": 14187428934500390688, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_astype.py": 8505605634958454768, + "venv/lib/python3.13/site-packages/psycopg2/tz.py": 9966680685796585198, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/theme.py": 14225878259183479398, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/accessors.py": 12743680296510533030, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_repr.py": 5577699046802107144, + "venv/lib/python3.13/site-packages/pandas/io/sql.py": 12592024569011796790, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/npyio.pyi": 15811156298637131744, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/RECORD": 16558021503346615951, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/dml.py": 6567627855667533645, + "venv/lib/python3.13/site-packages/pygments/lexers/kuin.py": 2818054539831699366, + "venv/lib/python3.13/site-packages/pygments/lexers/dsls.py": 11696359794961833087, + "frontend/build/_app/immutable/nodes/9.DlRMGlsh.js": 2466848614996704835, + "venv/lib/python3.13/site-packages/pandas/core/arrays/interval.py": 16281221279220043867, + "venv/lib/python3.13/site-packages/pandas/_libs/json.pyi": 10989301892466547548, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authorization_server.py": 13696038495666024258, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype.py": 7796703383405149774, + "venv/lib/python3.13/site-packages/anyio/to_interpreter.py": 8369454842687342401, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/core.py": 2199607970842512780, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_arithmetic.py": 17449383588949771785, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_diff.py": 7420674239386436885, + "frontend/build/_app/immutable/nodes/18.BWeHlFau.js": 1125159330861650302, + "frontend/build/_app/immutable/chunks/DeED_3EO.js": 8217702195318248974, + "backend/src/core/ws_log_handler.py": 7507069843556560249, + "backend/src/plugins/translate/_utils.py": 9570375434532627675, + "backend/tests/services/clean_release/test_report_audit_immutability.py": 4097909280121147581, + "venv/lib/python3.13/site-packages/pygments/formatters/img.py": 18230180171537565208, + "venv/lib/python3.13/site-packages/pandas/tests/test_common.py": 8987965560487646705, + "frontend/build/_app/immutable/chunks/CAMiRHtE.js": 8764442458195850407, + "venv/lib/python3.13/site-packages/pandas/_version_meson.py": 4875297137037630519, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py": 13742159245132241245, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_exceptions.py": 15290185665769490169, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_linalg.py": 2939739913218718132, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/groupby.py": 14661033005768429774, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string_arrow.py": 10008395772220468318, + "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py": 10703957986116846695, + "venv/lib/python3.13/site-packages/fastapi/_compat/may_v1.py": 9880083742927480960, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/retry.py": 17371295199208325375, + "backend/test_auth.db": 18214842050920965999, + "venv/lib/python3.13/site-packages/pydantic/_internal/_generate_schema.py": 7343380095251236503, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_to_timestamp.py": 17572983799929378095, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/provision.py": 2676876852726658063, + "venv/lib/python3.13/site-packages/pandas/tests/internals/test_api.py": 11217428888457119601, + "venv/lib/python3.13/site-packages/pandas/tests/reductions/__init__.py": 16150188643076000414, + "venv/lib/python3.13/site-packages/pytest_asyncio/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/wheel.py": 828222664427570058, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_bar.py": 7791068963246005463, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/python3.npy": 4957143805477405905, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/__init__.py": 237070345717942041, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/generic.py": 17755579497690180620, + "venv/lib/python3.13/site-packages/pygments/lexers/smithy.py": 10789533877863290582, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/RECORD": 8956798822782402202, + "venv/lib/python3.13/site-packages/h11/_readers.py": 832030916375670301, + "venv/lib/python3.13/site-packages/numpy/random/tests/__init__.py": 15130871412783076140, + "frontend/src/routes/tools/backups/+page.svelte": 6796050751535238723, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_as_unit.py": 4283520188709399244, + "venv/lib/python3.13/site-packages/pluggy/_callers.py": 1042673447718196223, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_datetime.py": 7456744745424297814, + "venv/lib/python3.13/site-packages/git/objects/blob.py": 8972374723153211283, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_any_index.py": 10800968419286329268, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/url.py": 1558176735588971829, + "venv/lib/python3.13/site-packages/multipart/__init__.py": 6496114190873890851, + "venv/lib/python3.13/site-packages/numpy/testing/tests/test_utils.py": 5291132432383518704, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_setitem.py": 2059440410065814114, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/expression.py": 15511250785671695359, + "venv/lib/python3.13/site-packages/pygments/styles/coffee.py": 17584414468786822371, + "venv/lib/python3.13/site-packages/pygments/lexers/bdd.py": 5389493859905397060, + "venv/lib/python3.13/site-packages/ecdsa/util.py": 10543788809724099209, + "backend/src/models/dataset_review_pkg/_session_models.py": 3332173837386512819, + "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/integration.py": 3085827906490477329, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/idnadata.py": 1169745920682458213, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_format.py": 16150524965028369071, + "venv/lib/python3.13/site-packages/anyio/_core/_tasks.py": 14104828341875599130, + "venv/lib/python3.13/site-packages/ecdsa/errors.py": 11755424742691057760, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/control.py": 2034518223411945438, + "venv/lib/python3.13/site-packages/pygments/styles/native.py": 4559566444883341527, + "venv/lib/python3.13/site-packages/pandas/core/indexes/category.py": 18192569199634406001, + "venv/lib/python3.13/site-packages/pandas/plotting/__init__.py": 13230487326686617492, + "frontend/build/_app/immutable/chunks/DNK-l8sb.js": 12498166761664912995, + "venv/lib/python3.13/site-packages/pandas/_libs/internals.cpython-313-x86_64-linux-gnu.so": 2008633361686956377, + "venv/lib/python3.13/site-packages/passlib/utils/des.py": 2650106381178222229, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/requests.py": 1102963893193914979, + "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/apps.py": 4219758952914498116, + "venv/lib/python3.13/site-packages/_pytest/debugging.py": 16135285365306979060, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_tzconversion.py": 7205566353454584259, + "venv/lib/python3.13/site-packages/pygments/styles/manni.py": 11853576691280294949, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/METADATA": 15876173086384975957, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/__init__.py": 9884041726796476720, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/__init__.py": 2635182244050911910, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_compression.py": 8061288329598682752, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_longtable.tpl": 12723621983797415444, + "venv/lib/python3.13/site-packages/numpy/core/__init__.py": 10397294636564169377, + "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.py": 5583478355093966990, + "frontend/build/_app/immutable/chunks/WFgopCIu.js": 4767118587366045802, + "venv/lib/python3.13/site-packages/anyio/_core/_resources.py": 8463932838064724564, + "venv/lib/python3.13/site-packages/h11/_util.py": 9362339074207904329, + "venv/lib/python3.13/site-packages/pandas/core/sorting.py": 689057022713741051, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py": 9331389900588353667, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cosh.csv": 9287774220881887502, + "venv/lib/python3.13/site-packages/pygments/lexers/theorem.py": 15666523910367201285, + "venv/lib/python3.13/site-packages/charset_normalizer/__main__.py": 8887772289570078278, + "backend/src/core/task_manager/cleanup.py": 8316318318890394605, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py": 7638945613579396718, + "venv/lib/python3.13/site-packages/dateutil/utils.py": 7067026915090881998, + "frontend/src/routes/datasets/review/ReviewWorkspaceRightRail.svelte": 13437011381072252917, + "frontend/test-results/translation.e2e.js-Transla-49baf-te-a-translation-job-via-UI-chromium/trace.zip": 14597002665264983691, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_matmul.py": 16169282225324014901, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_util.py": 6573034224727513309, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/misc.py": 6571241778569465264, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/package_data.py": 3078502124870585867, + "venv/lib/python3.13/site-packages/pygments/lexers/trafficscript.py": 776604244051744646, + "backend/src/plugins/translate/_run_service.py": 9256226506466064345, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py": 17765129030290410902, + "venv/lib/python3.13/site-packages/pygments/lexers/wren.py": 6259586056260713984, + "backend/src/plugins/mapper.py": 1970475023222782681, + "venv/lib/python3.13/site-packages/websockets/asyncio/connection.py": 17044409627448400723, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/ext.py": 15699809032725838985, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_generic.py": 1098361087956787007, + "venv/lib/python3.13/site-packages/ecdsa/test_eddsa.py": 1785854649532849946, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/registration.py": 14639972512303032712, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets.py": 10923534235697247269, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/LICENSE": 16174281248426886206, + "venv/lib/python3.13/site-packages/attrs/validators.py": 10346563361083429998, + "venv/lib/python3.13/site-packages/numpy/_core/shape_base.py": 3996519099530770308, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_shift.py": 8547819448301023303, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_replace.py": 8165088653532047247, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_indexing.py": 16264430536704066847, + "venv/lib/python3.13/site-packages/starlette/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oidc/registration/__init__.py": 9448552677419908109, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/coercions.py": 5112358449376598662, + "venv/lib/python3.13/site-packages/pandas/core/internals/managers.py": 13547912653290063698, + "venv/lib/python3.13/site-packages/pandas/io/parsers/arrow_parser_wrapper.py": 11745253171984606573, + "venv/lib/python3.13/site-packages/urllib3/exceptions.py": 9687187566231423953, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/RECORD": 6488682447051657004, + "venv/lib/python3.13/site-packages/uvicorn/workers.py": 7213290478118197047, + "backend/src/schemas/profile.py": 7950698901810733002, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/types.py": 356973910046950951, + "venv/lib/python3.13/site-packages/httpx/_utils.py": 4836536051177895209, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_converters.py": 8869440774594127419, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/instrumentation.py": 14882045449667214075, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/numerictypes.pyi": 15565573472138344755, + "backend/src/core/task_manager/models.py": 14810225773071673070, + "venv/lib/python3.13/site-packages/pandas/io/spss.py": 864838144958947906, + "backend/src/models/report.py": 9185954411073383239, + "venv/lib/python3.13/site-packages/pyasn1/error.py": 11036037300994516747, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunc_config.pyi": 11754104063443791746, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/hstore.py": 9700253004947691961, + "venv/lib/python3.13/site-packages/pip/_internal/network/auth.py": 6159841209618753296, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_info.py": 15151894356374015812, + "venv/lib/python3.13/site-packages/fastapi/middleware/gzip.py": 436886407566263171, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_time.py": 10463741033950978270, + "venv/lib/python3.13/site-packages/tzlocal/win32.py": 1949985224569608978, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_strptime.py": 14328510310436552635, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/state_changes.py": 11425629729736599320, + "backend/src/services/notifications/providers.py": 5844970860921555598, + "venv/lib/python3.13/site-packages/greenlet/TGreenletGlobals.cpp": 17400526028982558937, + "venv/lib/python3.13/site-packages/jeepney/bus.py": 5163075760432139100, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_constructors.py": 7466113171256856591, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_s390_unix.h": 5161826643658505126, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_almost_equal.py": 9333355453468475330, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop_duplicates.py": 5874427993953991216, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pyx": 11739218099400096619, + "venv/lib/python3.13/site-packages/git/refs/log.py": 5175801243332619268, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/connection.py": 15405047955974108860, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/token.py": 6987328478153445584, + "backend/src/plugins/translate/__tests__/test_token_budget.py": 9612693566837223655, + "docker/frontend.Dockerfile": 9514032629617942525, + "scripts/build_offline_docker_bundle.sh": 9577721838839060974, + "venv/lib/python3.13/site-packages/anyio/abc/_subprocesses.py": 4297459042599291531, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/tree.py": 4991806303236874829, + "venv/lib/python3.13/site-packages/numpy/ma/mrecords.pyi": 8945514863366384484, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_sorting.py": 8944017570338511677, + "venv/lib/python3.13/site-packages/pandas/io/formats/_color_data.py": 15614976586613738953, + "frontend/build/_app/immutable/nodes/4.BWeDuzI8.js": 3273334003166567943, + "venv/lib/python3.13/site-packages/numpy/ma/__init__.py": 214772578978182427, + "backend/src/core/auth/logger.py": 9289916075840905359, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_chaining_and_caching.py": 15814259766479305290, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_compression.py": 3167245427092658522, + "backend/src/models/filter_state.py": 6470311179852241297, + "venv/lib/python3.13/site-packages/pydantic/v1/error_wrappers.py": 5701679786078971939, + "venv/lib/python3.13/site-packages/fastapi/encoders.py": 1914192268055730299, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/provision.py": 9472538482404392519, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/pubprivmod.f90": 8680230823849248800, + "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so": 2888939866336522308, + "frontend/src/lib/stores/__tests__/test_activity.js": 15599963775430388097, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_reduction.py": 1049551473665255648, + "venv/lib/python3.13/site-packages/numpy/core/multiarray.py": 18368443598659711836, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_indexing.py": 14717641281147152159, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_formats.py": 5259100336852479395, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/properties.py": 5799883762127872919, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multithreading.py": 12547048606098186681, + "venv/lib/python3.13/site-packages/cffi/pkgconfig.py": 16057517851163939571, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_packbits.py": 7592956764084658562, + "venv/lib/python3.13/site-packages/pandas/io/stata.py": 14370692318835340323, + "venv/lib/python3.13/site-packages/pandas/io/sas/sasreader.py": 12766780210114366112, + "venv/lib/python3.13/site-packages/keyring/backends/SecretService.py": 17482492072907438453, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_select.py": 17823338162428379468, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.py": 12636954896699095212, + "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.cpython-313-x86_64-linux-gnu.so": 16604252871044637047, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/wheel.py": 13979536782410081712, + "venv/lib/python3.13/site-packages/pandas/core/util/hashing.py": 13106575357604888913, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_get_dummies.py": 16112674981222844375, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/serializer.py": 8708767945343850816, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/flatiter.py": 3066469329738423021, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args_and_kwargs.py": 3381939712123764154, + "venv/lib/python3.13/site-packages/pygments/lexers/automation.py": 8021926838024836904, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nep50_promotions.py": 6318578947214302182, + "frontend/src/lib/components/layout/__tests__/test_topNavbar.svelte.js": 11053755879172333925, + "frontend/build/_app/immutable/nodes/6.CtHJXRDQ.js": 1105946719271143546, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/METADATA": 9586902338916246692, + "venv/lib/python3.13/site-packages/pandas/core/_numba/extensions.py": 4132402557064663751, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_holiday.py": 8541197358003711845, + "venv/lib/python3.13/site-packages/pyasn1/type/tag.py": 13912273274509186651, + "venv/lib/python3.13/site-packages/anyio/abc/_resources.py": 8330687082933028623, + "venv/lib/python3.13/site-packages/pygments/lexers/praat.py": 54226477748516386, + "venv/lib/python3.13/site-packages/pandas/core/ops/docstrings.py": 3721650738518391256, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_period.py": 7727606662611927293, + "venv/bin/pip3.13": 13861749540792881808, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/parameters.py": 12219404446503940066, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/errors.py": 16711622442922413520, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_m68k_gcc.h": 6580173800099450404, + "venv/lib/python3.13/site-packages/websockets/__init__.py": 4916307256304054040, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/hotp.py": 18325640697829098192, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE": 10253862887434903467, + "venv/lib/python3.13/site-packages/ecdsa/ecdsa.py": 9770689913380927623, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_timedelta.py": 12822713918114715561, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.pyi": 15114028503984522176, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_quantile.py": 11966156706220948315, + "venv/lib/python3.13/site-packages/pycparser/ply/yacc.py": 4218889705926129841, + "venv/bin/fastapi": 12400009726251862770, + "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.pyi": 7674087576653964854, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_strings.py": 7379299916213794543, + "venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py": 15555781487631855604, + "venv/lib/python3.13/site-packages/fastapi/templating.py": 10760135144345238340, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/validator.py": 11486125578281617670, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_compat.py": 4677414058216516380, + "venv/lib/python3.13/site-packages/pandas/_libs/sparse.cpython-313-x86_64-linux-gnu.so": 2051484129454294720, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_comparison.py": 5348781442514871089, + "venv/lib/python3.13/site-packages/uvicorn/loops/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/styles/vs.py": 4188729978573652106, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/RECORD": 11788532857617688351, + "frontend/build/_app/immutable/chunks/BpdmWLbs.js": 1386912453729835223, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/scalar_string.f90": 15018631706178918578, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/__init__.py": 16532856661318424631, + "venv/lib/python3.13/site-packages/requests/packages.py": 7733041271118298220, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_groupby_shift_diff.py": 1210657479716368827, + "venv/lib/python3.13/site-packages/referencing/tests/test_jsonschema.py": 9815500901482570619, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py": 6509914623926583629, + "venv/lib/python3.13/site-packages/pygments/lexers/go.py": 11118104397271513417, + "backend/git_repos/remote/test-repo.git/refs/heads/feature/test": 7714224145564125988, + "venv/lib/python3.13/site-packages/pydantic/experimental/pipeline.py": 9493054841465227788, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraysetops.py": 10588903945244480732, + "venv/lib/python3.13/site-packages/pandas/core/indexers/utils.py": 5861072573716897447, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/metadata.py": 7204343250279221076, + "venv/lib/python3.13/site-packages/authlib/oidc/core/util.py": 5364455058429522633, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_timedeltaindex.py": 2359681317403710563, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_constructors.py": 17219764538851313252, + "venv/lib/python3.13/site-packages/pandas/io/api.py": 8275376846208437741, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.APACHE": 8359973023624924624, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_multiarray.py": 3592197154340095431, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/compiler.py": 13924519162400330589, + "venv/lib/python3.13/site-packages/pygments/lexers/tact.py": 1164971357219795420, + "venv/lib/python3.13/site-packages/numpy/lib/format.pyi": 11752225430594536662, + "venv/lib/python3.13/site-packages/pandas/_libs/tslib.pyi": 16170606956605100142, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_subclass.py": 11096337912872573314, + "venv/lib/python3.13/site-packages/pygments/lexers/_scilab_builtins.py": 17453812809700086888, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE": 13479831069780783137, + "frontend/src/lib/stores/assistantChat.js": 13807808925025688939, + "venv/lib/python3.13/site-packages/itsdangerous/url_safe.py": 13454753900702274266, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_null_file.py": 6885565068723353414, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_series.py": 2980850649986213051, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_indexing.py": 17106010337164216722, + "venv/lib/python3.13/site-packages/charset_normalizer/utils.py": 11724147997933584095, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/reflection.py": 5951113078754792806, + "venv/lib/python3.13/site-packages/pygments/sphinxext.py": 2768161754523156130, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nlargest.py": 14518132768753249360, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/fastapi/security/api_key.py": 6965937678104291169, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_constructors.py": 6440526981746306640, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_indexing_slow.py": 12558235638488427077, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.pyi": 14233146512272397376, + "frontend/src/routes/datasets/review/review-workspace-helpers.js": 8577563379877694502, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_frequencies.py": 14039315221824123200, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/unitofwork.py": 3177255974394221645, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_reshape.py": 5572426834735088916, + "venv/lib/python3.13/site-packages/psycopg2/__init__.py": 15798412693009721199, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/extending.py": 9859383893070793914, + "frontend/src/routes/admin/settings/+page.svelte": 15536865086528222650, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py": 17873236864291624476, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.pyi": 6616985736007157309, + "venv/lib/python3.13/site-packages/urllib3/contrib/socks.py": 645086578806871685, + "venv/lib/python3.13/site-packages/jaraco/functools/__init__.py": 15334412919279565528, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_mixins.py": 12351373879926460719, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/__init__.py": 11709551233929312136, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/replace.py": 1708865864686705833, "venv/lib/python3.13/site-packages/typing_inspection/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_isoc.py": 4923640657296076239 + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cbrt.csv": 8929553891827331780, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_duplicate_labels.py": 10663105616336167414, + "venv/lib/python3.13/site-packages/urllib3/util/request.py": 9432067988603015849, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/modules.py": 9023244176010175562, + "venv/lib/python3.13/site-packages/numpy/fft/tests/test_pocketfft.py": 18025182347224285050, + "venv/lib/python3.13/site-packages/pygments/lexers/eiffel.py": 8964563077187051946, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_categorical.py": 9923236395692943732, + "backend/src/api/routes/translate/_helpers.py": 13265933139767815312, + "backend/git_repos/remote/test-repo.git/objects/a7/49f11fad695dff77faf1708c7320867d0be1df": 4641609158325033031, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.cpython-313-x86_64-linux-gnu.so": 11019871270094058038, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/inference.py": 2786441797589123900, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/LICENSE": 16343563559291870811, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_dml_constructors.py": 4497038002189188311, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/scoping.py": 18223258186431020473, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odf.py": 15484940480924122966, + "venv/lib/python3.13/site-packages/pandas/io/parquet.py": 14415622950469421518, + "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.pyi": 15067467792538416274, + "venv/lib/python3.13/site-packages/pydantic/json.py": 4109352930301629308, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_real.f90": 9097998998863007705, + "venv/lib/python3.13/site-packages/pygments/formatters/html.py": 1918541918263449443, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/__init__.py": 96341051676048910, + "venv/lib/python3.13/site-packages/pandas/core/ops/missing.py": 9493665944764255854, + "frontend/src/routes/settings/MigrationMappingsTable.svelte": 4467573699012255407, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_version.pyi": 3636944488233043883, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_mod.f90": 5765181406654284736, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/index_tricks.pyi": 11463747060334597941, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_block_docstring.py": 18146459246837890489, + "frontend/src/routes/settings/git/__tests__/git_settings_page.ux.test.js": 5334904955024018051, + "venv/lib/python3.13/site-packages/pydantic/experimental/missing_sentinel.py": 1637241331832367709, + "venv/lib/python3.13/site-packages/numpy/lib/user_array.pyi": 1968484998382771986, + "frontend/src/routes/+layout.svelte": 2748734957389681795, + "backend/src/api/routes/translate/_correction_routes.py": 12437040676784563126, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion": 2607512361823362698, + "venv/lib/python3.13/site-packages/starlette/middleware/cors.py": 11384893784314197864, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_align.py": 10284128457194204574, + "backend/src/services/clean_release/publication_service.py": 1882863137548904782, + "venv/lib/python3.13/site-packages/websockets/speedups.pyi": 10322382420215791091, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/rsa_key.py": 7092106298733667895, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_win_type.py": 18105114803686556748, + "venv/lib/python3.13/site-packages/pandas/_libs/json.cpython-313-x86_64-linux-gnu.so": 2436285663630821963, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/RECORD": 5169907629260560698, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dh.pyi": 3254633032236269842, + "venv/lib/python3.13/site-packages/pandas/tests/extension/list/array.py": 8065856342131357421, + "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.pyi": 1424133074692438241, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_skew_kurt.py": 15225601437354712351, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_logical.py": 6818185860296232476, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/__init__.py": 16211693040174992594, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_free.f90": 8821492401453445757, + "venv/lib/python3.13/site-packages/httpcore/_async/interfaces.py": 18250473733460571816, + "venv/lib/python3.13/site-packages/numpy/strings/__init__.py": 6523674574341354095, + "venv/lib/python3.13/site-packages/httpcore/_backends/mock.py": 6849131425727092784, + "venv/lib/python3.13/site-packages/pygments/lexers/prolog.py": 599539463980699865, + "venv/lib/python3.13/site-packages/pygments/lexers/xorg.py": 14591144191879110061, + "frontend/src/lib/utils/debounce.js": 16884470786640632815, + "venv/lib/python3.13/site-packages/greenlet/greenlet_thread_support.hpp": 7773996382104849037, + "venv/lib/python3.13/site-packages/psycopg2/_json.py": 9729420372294281405, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/util.py": 6088868370298560146, + "venv/lib/python3.13/site-packages/jsonschema/tests/fuzz_validate.py": 2328585939822059352, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/liblber-1c9097e2.so.2.0.200": 2651879839738900594, + "venv/lib/python3.13/site-packages/idna/intranges.py": 12536174834761591006, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_polynomial.pyi": 9134949577567498894, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api_latest.c": 5472633890472031814, + "venv/lib/python3.13/site-packages/passlib/tests/test_win32.py": 13932956220195912963, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_get.py": 10255752806860493777, + "venv/lib/python3.13/site-packages/httpx/_transports/wsgi.py": 11981127094916412794, + "venv/lib/python3.13/site-packages/passlib/tests/sample1c.cfg": 13088198405762342506, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_indexing.py": 16291207621268530224, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/hist.py": 10928173425078493095, + "venv/lib/python3.13/site-packages/psycopg2/pool.py": 8855184580138330938, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_aix.h": 8706347224105630468, + "venv/lib/python3.13/site-packages/annotated_doc/__init__.py": 6834198874252778425, + "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.pyi": 15767546107856827543, + "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.pyi": 8232620851756907417, + "venv/lib/python3.13/site-packages/click/decorators.py": 2920877667855377423, + "venv/lib/python3.13/site-packages/numpy/tests/test_matlib.py": 14395481530627870882, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/console.py": 3540660073442483692, + "venv/lib/python3.13/site-packages/pandas/_libs/__init__.py": 17117112389651696580, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_add_prefix_suffix.py": 8367793476246001244, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_monotonic.py": 11817523305776951162, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_partial_indexing.py": 4132091154215910029, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_setitem.py": 10389770446834773837, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/relationships.py": 8401569216905291119, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/RECORD": 7810411361439269754, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_apply.py": 7511915990531229598, + "venv/lib/python3.13/site-packages/charset_normalizer/api.py": 9172704759115759638, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.pyi": 17528421279543420867, + "venv/lib/python3.13/site-packages/pygments/styles/arduino.py": 4578385147314541233, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_index.py": 10837492069089787028, + "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.py": 12796890117561096360, + "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.pyi": 12043292763005874815, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/shared.py": 1162754506656505196, + "venv/lib/python3.13/site-packages/pygments/formatters/latex.py": 7514533384468139434, + "frontend/src/lib/components/assistant/AssistantChatPanel.svelte": 4130047433623185888, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_no_deprecated_api.h": 7896028550808252043, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/visitors.py": 5190923415390076080, + "backend/src/models/dataset_review_pkg/_clarification_models.py": 2289021476863746270, + "venv/lib/python3.13/site-packages/numpy.libs/libquadmath-96973f99-934c22de.so.0.0.0": 5098713761946118139, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/conftest.py": 3691712173202219606, + "backend/src/plugins/translate/service_bulk_replace.py": 5355744069308185607, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/create.py": 16693261531832306870, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/concat.py": 14737724508458849795, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/entities.py": 8001287806688585081, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_freq_attr.py": 7685756734926749093, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/characteristics.py": 2916810597149066609, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_mrecords.py": 15952147778503828707, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_interval.py": 2613503970235591726, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_indexing.py": 7181087459615188551, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex_like.py": 13803156447991282315, + "venv/lib/python3.13/site-packages/PIL/_binary.py": 14039167720582222043, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunclike.pyi": 14623308681600207918, + "venv/lib/python3.13/site-packages/uvicorn/loops/uvloop.py": 5385425357504191161, + "venv/bin/keyring": 1488968652276547659, + "frontend/e2e/helpers/api.helper.js": 12740720187461301874, + "backend/src/plugins/translate/preview_executor.py": 7636554040403814604, + "backend/tests/test_translate_scheduler.py": 14274816721941617887, + "venv/lib/python3.13/site-packages/secretstorage/dhcrypto.py": 8860116996905452758, + "venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py": 15244280495983216204, + "venv/lib/python3.13/site-packages/numpy/core/overrides.pyi": 1968648615494204783, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_arrow_interface.py": 9757086292358759236, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename_axis.py": 12432900682994622455, + "venv/lib/python3.13/site-packages/certifi/cacert.pem": 14727832973577663826, + "venv/lib/python3.13/site-packages/jeepney/bus_messages.py": 5871341372541525842, + "venv/lib/python3.13/site-packages/pygments/styles/staroffice.py": 11694998942433220413, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_day.py": 6371851881186970754, + "venv/lib/python3.13/site-packages/pip/_internal/models/__init__.py": 13482731985065116750, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/dtypes.py": 9839461501970023861, + "venv/lib/python3.13/site-packages/pandas/_config/config.py": 1494288055210911451, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_inference.py": 16402427186697071465, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/decoder.py": 12653470212935324322, + "venv/lib/python3.13/site-packages/anyio/streams/tls.py": 4191898645554875562, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_filters.py": 17271344092069736999, + "venv/lib/python3.13/site-packages/yaml/parser.py": 18021832376874455801, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_cli.py": 3173259257428855556, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_coercion.py": 7261787308019145312, + "venv/lib/python3.13/site-packages/pygments/lexers/_php_builtins.py": 2344562378679281201, + "backend/git_repos/remote/test-repo.git/objects/5a/82ef700fef88b69cd480e4025283809496ee20": 7003383965314354150, + "venv/lib/python3.13/site-packages/pip/_internal/utils/_jaraco_text.py": 9046755357011730699, + "venv/lib/python3.13/site-packages/pandas/tests/extension/date/__init__.py": 16789767433439616593, + "frontend/src/pages/Settings.svelte": 12818597464628135154, + "venv/lib/python3.13/site-packages/pandas/io/json/_table_schema.py": 3103776773020058805, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/__init__.py": 12793458753561114666, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/authorization_server.py": 4427151727224472650, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_editable.py": 14352954849821931255, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_algs.py": 2479791018067637690, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_protocols.py": 5019018645804313814, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/accesstype.f90": 877982462186396830, + "venv/lib/python3.13/site-packages/pandas/core/window/__init__.py": 15180240812077877933, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_set_value.py": 17480833128128893268, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/dml.py": 6519732236745017861, + "venv/lib/python3.13/site-packages/pygments/lexers/tlb.py": 1169242051301155885, + "venv/lib/python3.13/site-packages/h11/_headers.py": 7592730288271984585, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/numpyconfig.h": 13339221870252345205, + "venv/lib/python3.13/site-packages/passlib/handlers/phpass.py": 16489430001449160264, + "backend/src/plugins/translate/plugin.py": 9225230324052126034, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286_bc.pyf": 10893958728966010450, + "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector.py": 15458248567257759500, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_coercion.py": 3339075591248270762, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_constructors.py": 16197656939536784142, + "venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py": 450504439085405418, + "venv/lib/python3.13/site-packages/gitdb/fun.py": 55226330810782184, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.c": 11535375410441493281, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/structures.py": 2668010839316715865, + "venv/lib/python3.13/site-packages/git/__init__.py": 15867730798565725003, + "venv/lib/python3.13/site-packages/_pytest/main.py": 931018496411984909, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/warnings_and_errors.py": 11172336980866895356, + "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.pyi": 9291444594181042963, + "venv/lib/python3.13/site-packages/pip/_internal/cli/index_command.py": 15798187258841839292, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_normalize.py": 11373923576259976755, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge.py": 11366935134260232329, + "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.pyi": 8916922961301555974, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hi77.f": 10617763407280454334, + "venv/lib/python3.13/site-packages/_pytest/runner.py": 5772610501857496741, + "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/asn1.py": 8801155055979135092, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexing.py": 5910217172351020811, + "venv/lib/python3.13/site-packages/starlette/endpoints.py": 3379755567024530053, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py": 9964642152916165317, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/__init__.py": 15340047930888693984, + "venv/lib/python3.13/site-packages/PIL/DdsImagePlugin.py": 10646832373516499548, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/foo.f90": 14491312943942432024, + "venv/lib/python3.13/site-packages/pygments/styles/stata_light.py": 16319330015324455277, + "venv/lib/python3.13/site-packages/pygments/lexers/erlang.py": 11757064262773184741, + "venv/lib/python3.13/site-packages/pygments/lexers/_cl_builtins.py": 16540712925368297957, + "venv/lib/python3.13/site-packages/pygments/lexers/robotframework.py": 11507713622179102496, + "venv/lib/python3.13/site-packages/pygments/lexers/haskell.py": 8011341851439354724, + "venv/lib/python3.13/site-packages/pygments/lexers/asc.py": 10744437946030875496, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/unused_registry.py": 10153622516112471812, + "venv/lib/python3.13/site-packages/sqlalchemy/util/preloaded.py": 3719702436309355562, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/azure.py": 13529763490186254462, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_multiplier.f": 13432803601367981068, + "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.py": 893743087939984792, + "venv/lib/python3.13/site-packages/pandas/core/interchange/column.py": 9498044216416177656, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_insert.py": 9195027887301180927, + "venv/lib/python3.13/site-packages/pygments/styles/borland.py": 17570558758932580659, + "venv/lib/python3.13/site-packages/packaging/_musllinux.py": 2568338440457080365, + "venv/lib/python3.13/site-packages/gitdb/__init__.py": 15381521749253695923, + "venv/lib/python3.13/site-packages/more_itertools/__init__.pyi": 16479506520351014608, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/METADATA": 15888766579203341778, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/kind/foo.f90": 1367964042730902899, + "venv/lib/python3.13/site-packages/pandas/tests/io/conftest.py": 5864037534050136837, + "frontend/src/lib/i18n/index.ts": 2428391315568750665, + "frontend/src/routes/datasets/review/[id]/+page.svelte": 3248469759382215571, + "frontend/src/components/git/GitManager.svelte": 2759220622954168718, + "frontend/build/_app/immutable/chunks/DoN5Iioi.js": 15272261932303525049, + "venv/lib/python3.13/site-packages/pygments/style.py": 17393199235575070210, + "backend/tests/test_task_persistence.py": 1764267414712263025, + "venv/lib/python3.13/site-packages/httpx/_client.py": 1063593889704982038, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_format.py": 5231304674283854194, + "venv/lib/python3.13/site-packages/more_itertools/recipes.py": 5254043689078173976, + "venv/lib/python3.13/site-packages/attr/filters.pyi": 13942974755090135724, + "backend/src/services/clean_release/source_isolation.py": 18208654007489616188, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/METADATA": 13428590545328703138, + "venv/lib/python3.13/site-packages/anyio/streams/file.py": 15232345967545920863, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/keys.pyi": 1846673463183660140, + "backend/src/plugins/translate/_batch_sizer.py": 14803922067332951736, + "venv/lib/python3.13/site-packages/_pytest/_io/wcwidth.py": 13515196388826152383, + "venv/lib/python3.13/site-packages/pandas/compat/pickle_compat.py": 11073483984394766474, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimelike.py": 5317270578347271174, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nested_sequence.pyi": 11245460325970663922, + "venv/lib/python3.13/site-packages/ecdsa/test_curves.py": 6577762025900435885, + "frontend/build/_app/immutable/nodes/29.BCwh4wDu.js": 10880930024902682923, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_constructors.py": 11065058462752831997, + "backend/src/services/notifications/__tests__/test_notification_service.py": 2377464079178202565, + "backend/src/app.py": 4855165033640359841, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/client_mixin.py": 17980753662107479779, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py": 4633931060151989960, + "venv/lib/python3.13/site-packages/numpy/lib/npyio.pyi": 3212440968296555560, + "venv/lib/python3.13/site-packages/cffi/_cffi_include.h": 13094899814611950155, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/version.py": 14723293207188512766, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test__datasource.py": 1747755531265478106, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/memmap.pyi": 2834143910417841603, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_reductions.py": 2816859231048302619, + "venv/lib/python3.13/site-packages/pip/_internal/cli/parser.py": 1064209424435927674, + "frontend/build/_app/immutable/nodes/1.B8QydpXl.js": 6880647685586948034, + "venv/lib/python3.13/site-packages/pandas/_libs/join.pyi": 369262334342439364, + "venv/lib/python3.13/site-packages/pygments/lexers/_lua_builtins.py": 12653747099659257203, + "venv/lib/python3.13/site-packages/numpy/typing/tests/test_isfile.py": 14544060093268573418, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_shape_base.py": 8837845103338426570, + "venv/lib/python3.13/site-packages/pandas/_config/dates.py": 3863381688916127265, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_constructors.py": 13890224577680389718, + "backend/tests/services/clean_release/test_compliance_execution_service.py": 5924200441771414883, + "venv/lib/python3.13/site-packages/pygments/lexers/jsx.py": 13855228003748707341, + "backend/src/plugins/translate/preview_review.py": 5584592578627646560, + "venv/lib/python3.13/site-packages/pip/_internal/network/session.py": 293771075394156394, + "venv/lib/python3.13/site-packages/numpy/_core/records.pyi": 14194354215405544730, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reset_index.py": 14490875921264161445, + "frontend/build/_app/immutable/nodes/33.iYr8xRUi.js": 4371667319710520041, + "venv/lib/python3.13/site-packages/pip/_internal/utils/hashes.py": 17310331665429541812, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.f90": 1249061697041863692, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex.tpl": 8820045022551446255, + "venv/lib/python3.13/site-packages/pygments/lexers/minecraft.py": 10133579583270214110, + "frontend/src/components/TaskList.svelte": 3024343043730631288, + "venv/lib/python3.13/site-packages/pygments/lexers/__init__.py": 4627540245914849670, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_count.py": 9186538441380080087, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/markers.py": 5247208668583898949, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/parameters.py": 1647490964392525933, + "venv/lib/python3.13/site-packages/pydantic/v1/fields.py": 3948219773415054006, + "venv/lib/python3.13/site-packages/numpy/_core/printoptions.py": 3897105663572772838, + "venv/lib/python3.13/site-packages/starlette/schemas.py": 697416731579555195, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_indexing.py": 14461606056665485421, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_conversion.py": 7507083194555424923, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.pyi": 4432290176776357346, + "backend/src/plugins/translate/preview_llm_client.py": 14412557858942627249, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE": 4672913476475760762, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_groupby.py": 15721254196883995634, + "venv/lib/python3.13/site-packages/requests/compat.py": 5836590172733661320, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/asymmetric_key.py": 14948661854506285135, + "venv/lib/python3.13/site-packages/uvicorn/__init__.py": 11517239807926185665, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/__init__.py": 1923528231486706442, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log10.csv": 13441294827017400897, + "venv/lib/python3.13/site-packages/pandas/tests/frame/conftest.py": 15090201792072045503, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_set.py": 14375107704596905983, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_conversion.py": 12184280074522274780, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/information_schema.py": 8551227132710897346, + "backend/src/plugins/llm_analysis/plugin.py": 17227799960135116601, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asof.py": 17616601748321325187, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_astype.py": 15768655715952923925, + "docker/all-in-one.Dockerfile": 10896793266219904099, + "venv/lib/python3.13/site-packages/ecdsa/__init__.py": 6237633580090653788, + "venv/lib/python3.13/site-packages/pandas/core/internals/construction.py": 893951849732478990, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_quantile.py": 17110581927381599978, + "venv/lib/python3.13/site-packages/pandas/tests/test_aggregation.py": 16863055892192070549, + "venv/lib/python3.13/site-packages/PIL/IcnsImagePlugin.py": 1870235606309784691, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/INSTALLER": 17282701611721059870, + "frontend/build/_app/immutable/nodes/10.COlRD-Eh.js": 13513237299722070120, + "frontend/build/_app/immutable/chunks/B19Q4MUf.js": 8384959676363720729, + "backend/src/api/routes/git/_helpers.py": 6888678759460652166, + "venv/lib/python3.13/site-packages/pillow.libs/libbrotlicommon-c55a5f7a.so.1.2.0": 2635192149914499986, + "venv/lib/python3.13/site-packages/pyasn1/codec/cer/decoder.py": 17559787396729804535, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_between.py": 337429135216881486, + "venv/lib/python3.13/site-packages/smmap/test/test_mman.py": 8977119225749253844, + "venv/lib/python3.13/site-packages/pandas/io/excel/_openpyxl.py": 6676451564208867775, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_sorted.py": 1899608688401238388, + "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/json.py": 4660603129768960095, + "venv/lib/python3.13/site-packages/passlib/handlers/mssql.py": 2800599582708170195, + "venv/lib/python3.13/site-packages/jeepney/bindgen.py": 7094585345946943534, + "backend/src/core/utils/async_network.py": 2763204398131017281, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/licenses/LICENSE.rst": 5960743202700406649, + "venv/lib/python3.13/site-packages/numpy/typing/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/expressions.py": 8015786996064244656, + "venv/lib/python3.13/site-packages/pandas/tests/test_sorting.py": 10751098438703044961, + "venv/lib/python3.13/site-packages/pandas/core/sample.py": 11913688303879867963, + "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.pyi": 4729765666018003634, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resample_api.py": 3278209206670699265, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_qcut.py": 8972418112914954758, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/type_api.py": 10299750173867399533, + "venv/lib/python3.13/site-packages/pygments/lexers/qvt.py": 1895735583818227972, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_arrayobject.py": 9578555381494297966, + "venv/lib/python3.13/site-packages/httpcore/_sync/socks_proxy.py": 4418971149260853240, + "venv/lib/python3.13/site-packages/pygments/lexers/sieve.py": 15757825442941567629, + "backend/src/services/clean_release/facade.py": 11231787492455424065, + "backend/test_app.db": 2486636812009447051, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_two_greenlets.py": 11156275429806391390, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/__init__.py": 16416360448593978041, + "venv/lib/python3.13/site-packages/pandas/_libs/interval.cpython-313-x86_64-linux-gnu.so": 8964760352224019544, + "venv/lib/python3.13/site-packages/jsonschema_specifications/_core.py": 14812112544772987164, + "venv/lib/python3.13/site-packages/pandas/_libs/interval.pyi": 15698173755300703836, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_construction.py": 13953424340346625390, + "venv/lib/python3.13/site-packages/more_itertools/more.pyi": 4844596605492825806, + "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.py": 7365943844063702766, + "backend/src/api/routes/__tests__/test_clean_release_v2_api.py": 13464989331176891726, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_half.py": 3121836756584726126, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arithmetic.py": 14858832264660410908, + "venv/lib/python3.13/site-packages/keyring/__init__.py": 991260689223167957, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo77.f": 16250175448122328648, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_lib.py": 6721066015604956469, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/conftest.py": 6137462119729731945, + "frontend/src/routes/reports/+page.svelte": 17831076184881844976, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/yaml.py": 17801117405950663611, + "venv/lib/python3.13/site-packages/pydantic/_internal/_fields.py": 2692892815545218335, + "backend/src/api/routes/git/_config_routes.py": 6093940098740660772, + "venv/lib/python3.13/site-packages/numpy/_pytesttester.pyi": 5750506937526308405, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/contains.py": 3098087827310439993, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/extension_types.py": 1856757457657310231, + "venv/lib/python3.13/site-packages/numpy/lib/scimath.pyi": 18165025261847276375, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py": 1945891789924713537, + "venv/lib/python3.13/site-packages/pygments/unistring.py": 18037463865539157041, + "frontend/src/routes/settings/connections/+page.svelte": 276661107137418305, + "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.pyi": 12144531601504755948, + "venv/lib/python3.13/site-packages/authlib/common/encoding.py": 52296066492753953, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iloc.py": 7586049606891014270, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_compound.f90": 15012692484070385863, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_retain_attributes.py": 16210262356844956764, + "venv/lib/python3.13/site-packages/pygments/lexers/lean.py": 14518351619544450286, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_case_justify.py": 8204537837649550062, + "venv/lib/python3.13/site-packages/keyring/compat/py312.py": 17581691618899404491, + "venv/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py": 3531093454660385247, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_numpy.py": 4073641912755651877, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_api.py": 16743710460729009914, + "venv/lib/python3.13/site-packages/numpy/typing/tests/test_runtime.py": 12534621594420844770, + "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django_source.py": 15577951591316518967, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_slice.py": 9936085512921529417, + "venv/lib/python3.13/site-packages/pandas/io/formats/style_render.py": 13586834368514801953, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pymysql.py": 12923876736367229454, + "venv/lib/python3.13/site-packages/packaging/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_set.py": 534498057184394558, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/plugin_base.py": 2880434680251108786, + "venv/lib/python3.13/site-packages/pygments/lexer.py": 3658450949169647228, + "venv/lib/python3.13/site-packages/pandas/compat/numpy/__init__.py": 1037113402100731082, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertions.py": 5556667947376168765, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_hour.py": 14128508606361694814, + "venv/lib/python3.13/site-packages/attr/setters.py": 12467854389745942025, + "venv/lib/python3.13/site-packages/requests/api.py": 12339139862411341381, + "venv/lib/python3.13/site-packages/iniconfig/_version.py": 8497474654470220170, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/api/test_types.py": 8889829773923216769, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_spss.py": 4865360315909532603, + "venv/lib/python3.13/site-packages/pygments/formatters/other.py": 13124903290103898118, + "venv/lib/python3.13/site-packages/pygments/lexers/_vim_builtins.py": 6842381248727719900, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/aead.pyi": 13061638961405460334, + "venv/lib/python3.13/site-packages/numpy/version.py": 1497063540123613501, + "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet_trash.py": 6191655521667576604, + "backend/git_repos/remote/test-repo.git/objects/ed/0432f88f5f7d95b26ecde34345abe741dd9db2": 13426916511127204463, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_formats.py": 13500155167624433622, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_is_monotonic.py": 12899848089975919515, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py": 14810883299259169406, + "venv/lib/python3.13/site-packages/httpx/_types.py": 12461152436147308761, + "venv/lib/python3.13/site-packages/passlib/hosts.py": 11171402820797380938, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/__init__.py": 10357835306532281617, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dh.py": 5728866271812501558, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/connection.py": 5582413226288987808, + "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_version.py": 849232015288488204, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_macosx.h": 3653129798702716366, + "venv/lib/python3.13/site-packages/_yaml/__init__.py": 3395829783751571350, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex.py": 8772423071604000084, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/_typing.py": 10524021526716767489, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/__init__.py": 9482393721453335937, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/__init__.py": 8807410829260170597, + "venv/lib/python3.13/site-packages/httpx/_exceptions.py": 18399489130669465891, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/__init__.py": 10834852742917144496, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein_py.py": 11350239659973489646, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/mongodb.py": 5310116401542377882, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_interaction.py": 12216477658994324787, + "venv/lib/python3.13/site-packages/passlib/handlers/fshp.py": 13298827601291507295, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/dtype.pyi": 12178008080078679769, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/base.py": 17992335652981439676, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/METADATA": 1266134223155266488, + "venv/lib/python3.13/site-packages/ecdsa/test_keys.py": 17117188373419689467, + "venv/lib/python3.13/site-packages/greenlet/greenlet_refs.hpp": 8525862687669040510, + "venv/lib/python3.13/site-packages/numpy/char/__init__.py": 4428218097234308768, + "venv/lib/python3.13/site-packages/websockets/asyncio/async_timeout.py": 7267209982032642028, + "venv/lib/python3.13/site-packages/pandas/core/ops/__init__.py": 4049432353810047365, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/etcd.py": 4078354986605069279, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_partial_slicing.py": 16219611534574666182, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml.py": 6906547816798138000, + "venv/lib/python3.13/site-packages/httpcore/_backends/anyio.py": 6167729964577005634, + "venv/lib/python3.13/site-packages/gitdb/exc.py": 13975852664048821278, + "venv/lib/python3.13/site-packages/pygments/styles/paraiso_dark.py": 3976779833379189837, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/f2cmap/isoFortranEnvMap.f90": 15768643271492041853, + "venv/lib/python3.13/site-packages/keyring/backend_complete.bash": 16589214602970214174, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/METADATA": 15658158392301945427, + "venv/lib/python3.13/site-packages/requests/utils.py": 17045260098091371478, + "venv/lib/python3.13/site-packages/PIL/ImagePalette.py": 9227837918026991107, + "venv/lib/python3.13/site-packages/pygments/lexers/openscad.py": 13045825115225232396, + "venv/lib/python3.13/site-packages/pygments/lexers/x10.py": 6167378515780115912, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/METADATA": 12295263493152296681, + "frontend/src/lib/components/translate/CorrectionCell.svelte": 14340498120654577399, + "frontend/src/components/git/DeploymentModal.svelte": 16301125393395343650, + "venv/lib/python3.13/site-packages/pandas/core/strings/accessor.py": 14021462534679430426, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/block.f": 10727418304254548946, + "backend/git_repos/remote/test-repo.git/hooks/prepare-commit-msg.sample": 7530253302780245896, + "backend/src/core/migration/__init__.py": 13075189211840127611, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/framework_integration.py": 3935346152156572025, + "venv/lib/python3.13/site-packages/pygments/lexers/int_fiction.py": 8806961041808606055, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/base.py": 16052592982063844259, + "venv/pyvenv.cfg": 2513607356761047446, + "venv/lib/python3.13/site-packages/pygments/lexers/special.py": 4972294202790455038, + "frontend/src/lib/components/reports/__tests__/reports_filter_performance.test.js": 10583581505638859039, + "frontend/src/routes/dashboards/[id]/components/DashboardLinkedResources.svelte": 8279080994517688482, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_py.py": 6518425342376455821, + "venv/lib/python3.13/site-packages/yaml/constructor.py": 8731384179016992045, + "frontend/src/services/toolsService.js": 1568142461283466080, + "venv/lib/python3.13/site-packages/annotated_doc/main.py": 3384049281532298500, + "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.pyi": 2104100025130695295, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py": 5895297076117159245, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_index_col.py": 17604191644943383567, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_numpy.py": 9825804096508014886, + "venv/lib/python3.13/site-packages/uvicorn/middleware/asgi2.py": 5355343704425405009, + "backend/tests/services/clean_release/test_compliance_task_integration.py": 682622548217611215, + "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/__init__.py": 63497920264089252, + "venv/lib/python3.13/site-packages/numpy/lib/_iotools.py": 10522826373041592861, + "venv/lib/python3.13/site-packages/pandas/io/excel/_odfreader.py": 2760916559416783928, + "venv/lib/python3.13/site-packages/git/index/fun.py": 18338379518768926372, + "venv/lib/python3.13/site-packages/pygments/lexers/maxima.py": 11824576541091274641, + "backend/src/api/routes/__tests__/test_dataset_review_api.py": 17381354465400611694, + "venv/lib/python3.13/site-packages/pygments/lexers/yara.py": 17510792615476493957, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA_py.py": 2494967094400350388, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_elffile.py": 5728213398707817049, + "venv/lib/python3.13/site-packages/keyring/errors.py": 4587094338672775040, + "venv/lib/python3.13/site-packages/pluggy/_manager.py": 18202550145040079612, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_astype.py": 16700461036658228009, + "venv/lib/python3.13/site-packages/itsdangerous/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pydantic_settings/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/_openssl.pyi": 4376871621615688626, + "venv/lib/python3.13/site-packages/pygments/filter.py": 16846063190466267618, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_extending.py": 10202091354750153497, + "venv/lib/python3.13/site-packages/jaraco/classes/meta.py": 15930255620248873425, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arrayterator.py": 1646530870049791291, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/oct_key.py": 11504245602045724, + "frontend/src/lib/components/reports/__tests__/report_detail.ux.test.js": 11878832350632571462, + "backend/git_repos/remote/test-repo.git/objects/cf/d2dcbd55735b8cedf21e04e64ac627c6f9a458": 10982669277486885525, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/stride_tricks.pyi": 5844806555773292985, + "venv/lib/python3.13/site-packages/fastapi/openapi/constants.py": 1111838716953068285, + "venv/lib/python3.13/site-packages/numpy/random/_common.pyi": 5094115692952609118, + "venv/lib/python3.13/site-packages/pygments/lexers/fortran.py": 6859638690245583968, + "backend/src/plugins/git/__init__.py": 2144831474780401625, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.py": 17112989332785349705, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_api.py": 7131036004750554905, + "venv/lib/python3.13/site-packages/pydantic/_internal/_dataclasses.py": 15256193972947054890, + "venv/lib/python3.13/site-packages/referencing/jsonschema.py": 14596871117484757502, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/__init__.py": 17454177076687312424, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_serialization.py": 2011519400081029270, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x25519.py": 18229054440703471493, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.pyi": 15789956857032062129, + "venv/lib/python3.13/site-packages/pandas/core/resample.py": 13814298871925341617, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_online.py": 11850497659869773510, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_orm_types.py": 5508682532707462217, + "venv/lib/python3.13/site-packages/urllib3/util/timeout.py": 17922362393077965047, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_extras.py": 16160090335191788104, + "venv/lib/python3.13/site-packages/passlib/tests/test_pwd.py": 10089138951415591061, + "venv/lib/python3.13/site-packages/attr/_config.py": 1864824728504480204, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_api.py": 5813060676255093777, + "backend/src/plugins/translate/orchestrator_sql.py": 15420456250044832567, + "backend/git_repos/remote/test-repo.git/refs/heads/preprod": 2288780285329550368, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/loading.py": 3460142472461437916, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_freq_attr.py": 1720704043778272895, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/help.py": 3124654001287175629, + "venv/lib/python3.13/site-packages/numpy/core/__init__.pyi": 15130871412783076140, + "backend/src/api/routes/profile.py": 7140384110744553926, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/helpconfig.py": 3033477806668470951, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/scanner.py": 5640381644804445932, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_longdouble.py": 9337218715436213122, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/RECORD": 11111103193315065053, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/introspection.py": 14645217065508644154, + "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_gather.py": 3634138701088855517, + "venv/lib/python3.13/site-packages/rsa/common.py": 10921682819204792805, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.h": 16138871162713175599, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_series.py": 16418555817529711079, + "venv/lib/python3.13/site-packages/urllib3/util/ssl_match_hostname.py": 7411350247004265632, + "venv/lib/python3.13/site-packages/gitdb/db/mem.py": 15823812455996199993, + "venv/lib/python3.13/site-packages/dotenv/ipython.py": 15904966766060247908, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_print.py": 598821365691498702, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/json.py": 14755215397454910426, + "venv/lib/python3.13/site-packages/attr/exceptions.pyi": 12323493441583681402, + "venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py": 7733356903092860495, + "venv/lib/python3.13/site-packages/authlib/common/security.py": 2291133227104016684, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/pygments/styles/_mapping.py": 11752660241391232497, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/LICENSE": 6581019367004699816, + "venv/lib/python3.13/site-packages/passlib/crypto/_md4.py": 379335134151814809, + "venv/lib/python3.13/site-packages/pandas/core/nanops.py": 18291534943985310005, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/constructors.py": 7587476800569414769, + "venv/lib/python3.13/site-packages/pygments/lexers/bqn.py": 11823648528816858738, + "venv/lib/python3.13/site-packages/attr/_version_info.pyi": 7141309436445210354, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x32_unix.h": 9165722966153119887, + "venv/lib/python3.13/site-packages/greenlet/TStackState.cpp": 16720079760074325527, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.py": 5564496700647900501, + "venv/lib/python3.13/site-packages/passlib/handlers/bcrypt.py": 8125664010268654055, + "venv/lib/python3.13/site-packages/bcrypt/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.pyi": 13173581775000480811, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_cat_accessor.py": 4304062895283374927, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_cumulative.py": 8178093067175155877, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/well_known.py": 7183795183137931709, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.pyi": 1826323011169529229, + "venv/lib/python3.13/site-packages/websockets/exceptions.py": 12048063047468402740, + "venv/lib/python3.13/site-packages/referencing/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/jose/errors.py": 15231759002942436632, + "venv/lib/python3.13/site-packages/fastapi/staticfiles.py": 4094330072433302614, + "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.pyi": 14558091964387571257, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_writers.py": 103545128256076790, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/state.py": 10500170103357297551, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ext.py": 8361831054662096695, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/boa.py": 14409319610243925864, + "venv/lib/python3.13/site-packages/pygments/lexers/sas.py": 6128008194712914874, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py": 7019253290755243568, + "venv/lib/python3.13/site-packages/pygments/lexers/kusto.py": 16285533608924436308, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/blocking.py": 3483388533957772681, + "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.pyi": 3661113433078225952, + "venv/lib/python3.13/site-packages/pydantic_settings/main.py": 17272314000918984790, + "venv/lib/python3.13/site-packages/pydantic/v1/json.py": 16596969681914729251, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo77.f": 12000068001851270396, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_values.py": 834951665649981397, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/base.py": 16891437410462671317, + "frontend/src/lib/api/__tests__/reports_api.test.js": 11820587361929895100, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/resource_protector.py": 11539718751174980004, + "frontend/src/lib/components/assistant/AssistantClarificationCard.svelte": 6722890639215637790, + "venv/lib/python3.13/site-packages/gitdb/test/test_util.py": 541144638852210407, + "frontend/src/assets/svelte.svg": 13872699371039797074, + "backend/src/core/config_manager.py": 18161236261317111528, + "venv/lib/python3.13/site-packages/multipart/decoders.py": 11346752866822986212, + "backend/src/api/routes/llm.py": 9139371360302897928, + "backend/src/services/clean_release/policy_engine.py": 3180817867716931725, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_fillna.py": 8802093795040069423, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_parser.py": 3540203683732744499, + "venv/lib/python3.13/site-packages/pygments/styles/__init__.py": 18383248976818885498, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_value_counts.py": 5534713464063604301, + "venv/lib/python3.13/site-packages/numpy/random/_sfc64.pyi": 3536657185495782642, + "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.cpython-313-x86_64-linux-gnu.so": 11074560904305737689, + "venv/lib/python3.13/site-packages/_pytest/mark/__init__.py": 9343015549300275714, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npz": 12369168483236619421, + "venv/lib/python3.13/site-packages/jeepney/io/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/ecdsa/ecdh.py": 12596317261561755793, + "venv/lib/python3.13/site-packages/numpy/random/__init__.py": 11732100564412485633, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_dtypes.py": 13065857166166716628, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_unary.py": 8046543893978198672, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_functions.py": 2255133375699086300, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/phix.py": 16775285097768256238, + "venv/lib/python3.13/site-packages/pygments/lexers/promql.py": 8929925488296216854, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/__init__.py": 4014740126161973332, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.py": 17993169044119596860, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_spinners.py": 15873416546130487469, + "venv/lib/python3.13/site-packages/attr/converters.py": 5179615793507170642, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/highlighter.py": 10615901258492570740, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/poly1305.py": 10854598965202354516, + "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.py": 7446626038222262489, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_missing.py": 1963705411068820866, + "venv/lib/python3.13/site-packages/keyring/testing/util.py": 11195868112805409554, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.pyi": 16372699564794732463, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pct_change.py": 10313980265441433173, + "backend/tests/test_smoke_app.py": 7819901244710945164, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi": 13850254283540522444, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tan.csv": 6233924374281534211, + "venv/lib/python3.13/site-packages/fastapi/datastructures.py": 727178955746650271, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.pyf": 10179237442840865225, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/sqlalchemy.py": 811968833735951956, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_twodim_base.py": 17623133613018058380, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiprocessing.py": 5522490547285108448, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/putmask.py": 10934101690788549526, + "venv/lib/python3.13/site-packages/_pytest/stepwise.py": 3527630676691417317, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_internals.py": 10472508540875131797, + "venv/lib/python3.13/site-packages/cryptography/fernet.py": 10137792148038140630, + "venv/lib/python3.13/site-packages/anyio/streams/buffered.py": 8916188172110578587, + "venv/lib/python3.13/site-packages/apscheduler/executors/debug.py": 14072221218785494847, + "backend/src/core/scheduler.py": 2472649939852521889, + "venv/lib/python3.13/site-packages/uvicorn/loops/asyncio.py": 16496916527760969173, + "venv/lib/python3.13/site-packages/PIL/XVThumbImagePlugin.py": 7472134175570205454, + "backend/src/api/routes/translate/_router.py": 4164547666727936398 } } \ No newline at end of file diff --git a/backend/delete_running_tasks.py b/backend/delete_running_tasks.py index 8e6bdca2..028b8921 100644 --- a/backend/delete_running_tasks.py +++ b/backend/delete_running_tasks.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 -# [DEF:DeleteRunningTasksUtil:Module] -# @PURPOSE: Script to delete tasks with RUNNING status from the database. -# @LAYER: Utility -# @SEMANTICS: maintenance, database, cleanup -# @RELATION: DEPENDS_ON ->[TasksSessionLocal] -# @RELATION: DEPENDS_ON ->[TaskRecord] +# #region DeleteRunningTasksUtil [C:3] [TYPE Module] +# @PURPOSE Script to delete tasks with RUNNING status from the database. +# @LAYER Infrastructure +# @SEMANTICS maintenance, database, cleanup +# @RELATION DEPENDS_ON ->[TaskRecord] +# @RELATION DEPENDS_ON ->[TaskRecord] from sqlalchemy.orm import Session from src.core.database import TasksSessionLocal from src.models.task import TaskRecord +# #region delete_running_tasks [C:4] [TYPE Function] +# @PURPOSE Delete all tasks with RUNNING status from the database. +# @PRE Database is accessible and TaskRecord model is defined. +# @POST All tasks with status 'RUNNING' are removed from the database. +# @SIDE_EFFECT Modifies DB: deletes RUNNING tasks. Prints to stdout. -# [DEF:delete_running_tasks:Function] -# @PURPOSE: Delete all tasks with RUNNING status from the database. -# @PRE: Database is accessible and TaskRecord model is defined. -# @POST: All tasks with status 'RUNNING' are removed from the database. def delete_running_tasks(): """Delete all tasks with RUNNING status from the database.""" session: Session = TasksSessionLocal() @@ -41,8 +42,9 @@ def delete_running_tasks(): print(f"Error deleting tasks: {e}") finally: session.close() +# #endregion delete_running_tasks # [/DEF:delete_running_tasks:Function] if __name__ == "__main__": delete_running_tasks() -# [/DEF:DeleteRunningTasksUtil:Module] +# #endregion DeleteRunningTasksUtil diff --git a/backend/src/api/auth.py b/backend/src/api/auth.py index 738c11eb..ab1b5eb2 100755 --- a/backend/src/api/auth.py +++ b/backend/src/api/auth.py @@ -1,12 +1,15 @@ -# #region AuthApi [C:3] [TYPE Module] [SEMANTICS fastapi, auth, api] -# +# #region AuthApi [C:5] [TYPE Module] [SEMANTICS fastapi, auth, api] # @BRIEF Authentication API endpoints. -# @LAYER: API -# @RELATION DEPENDS_ON -> [AuthService] -# @RELATION DEPENDS_ON -> [get_auth_db] -# @RELATION DEPENDS_ON -> [get_current_user] +# @LAYER API # @RELATION DEPENDS_ON -> [is_adfs_configured] -# @INVARIANT: All auth endpoints must return consistent error codes. +# @RELATION DEPENDS_ON -> [is_adfs_configured] +# @RELATION DEPENDS_ON -> [is_adfs_configured] +# @RELATION DEPENDS_ON -> [is_adfs_configured] +# @PRE Python environment and dependencies installed; database available. +# @POST FastAPI app instance with auth routes registered. +# @SIDE_EFFECT Registers API routes; configures OAuth and CORS middleware. +# @DATA_CONTRACT Input -> OAuth2PasswordRequestForm -> Token, User +# @INVARIANT All auth endpoints must return consistent error codes. import starlette.requests from fastapi import APIRouter, Depends, HTTPException, status @@ -29,12 +32,14 @@ router = APIRouter(prefix="/api/auth", tags=["auth"]) # #endregion router -# #region login_for_access_token [C:3] [TYPE Function] +# #region login_for_access_token [C:4] [TYPE Function] # @BRIEF Authenticates a user and returns a JWT access token. -# @PRE: form_data contains username and password. -# @POST: Returns a Token object on success. -# @RELATION CALLS -> [AuthService.authenticate_user] +# @PRE form_data contains username and password. +# @POST Returns a Token object on success. # @RELATION CALLS -> [AuthService.create_session] +# @RELATION CALLS -> [AuthService.create_session] +# @SIDE_EFFECT DB read/write for auth session; writes security event log. + @router.post("/login", response_model=Token) async def login_for_access_token( form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_auth_db) @@ -58,11 +63,13 @@ async def login_for_access_token( # #endregion login_for_access_token -# #region read_users_me [C:3] [TYPE Function] +# #region read_users_me [C:4] [TYPE Function] # @BRIEF Retrieves the profile of the currently authenticated user. -# @PRE: Valid JWT token provided. -# @POST: Returns the current user's data. +# @PRE Valid JWT token provided. +# @POST Returns the current user's data. # @RELATION DEPENDS_ON -> [get_current_user] +# @SIDE_EFFECT Reads current user from DB via auth middleware; writes security event log. + @router.get("/me", response_model=UserSchema) async def read_users_me(current_user: UserSchema = Depends(get_current_user)): with belief_scope("api.auth.me"): @@ -72,11 +79,13 @@ async def read_users_me(current_user: UserSchema = Depends(get_current_user)): # #endregion read_users_me -# #region logout [C:3] [TYPE Function] +# #region logout [C:4] [TYPE Function] # @BRIEF Logs out the current user (placeholder for session revocation). -# @PRE: Valid JWT token provided. -# @POST: Returns success message. +# @PRE Valid JWT token provided. +# @POST Returns success message. # @RELATION DEPENDS_ON -> [get_current_user] +# @SIDE_EFFECT Writes security event LOGOUT event. + @router.post("/logout") async def logout(current_user: UserSchema = Depends(get_current_user)): with belief_scope("api.auth.logout"): @@ -89,10 +98,12 @@ async def logout(current_user: UserSchema = Depends(get_current_user)): # #endregion logout -# #region login_adfs [C:3] [TYPE Function] +# #region login_adfs [C:4] [TYPE Function] # @BRIEF Initiates the ADFS OIDC login flow. -# @POST: Redirects the user to ADFS. +# @POST Redirects the user to ADFS. # @RELATION USES -> [is_adfs_configured] +# @SIDE_EFFECT Redirects user to ADFS external OIDC provider. + @router.get("/login/adfs") async def login_adfs(request: starlette.requests.Request): with belief_scope("api.auth.login_adfs"): @@ -108,12 +119,14 @@ async def login_adfs(request: starlette.requests.Request): # #endregion login_adfs -# #region auth_callback_adfs [C:3] [TYPE Function] +# #region auth_callback_adfs [C:4] [TYPE Function] # @BRIEF Handles the callback from ADFS after successful authentication. -# @POST: Provisions user JIT and returns session token. -# @RELATION DEPENDS_ON -> [is_adfs_configured] -# @RELATION CALLS -> [AuthService.provision_adfs_user] +# @POST Provisions user JIT and returns session token. # @RELATION CALLS -> [AuthService.create_session] +# @RELATION CALLS -> [AuthService.create_session] +# @RELATION CALLS -> [AuthService.create_session] +# @SIDE_EFFECT Provisions user in DB, creates auth session, writes security event log. + @router.get("/callback/adfs", name="auth_callback_adfs") async def auth_callback_adfs( request: starlette.requests.Request, db: Session = Depends(get_auth_db) diff --git a/backend/src/api/routes/__init__.py b/backend/src/api/routes/__init__.py index 726cf0cf..5e9b53b6 100755 --- a/backend/src/api/routes/__init__.py +++ b/backend/src/api/routes/__init__.py @@ -1,8 +1,10 @@ -# #region ApiRoutesModule [C:3] [TYPE Module] [SEMANTICS api, package, router, lazy, import] +# #region ApiRoutesModule [C:5] [TYPE Module] [SEMANTICS api, package, router, lazy, import] # @BRIEF Provide lazy route module loading to avoid heavyweight imports during tests. # @LAYER: API # @RELATION CALLS -> [ApiRoutesGetAttr] # @RELATION BINDS_TO -> [Route_Group_Contracts] +# @PRE: FastAPI app initialized, route modules available in package +# @POST: Route modules are lazily loadable via __getattr__ # @INVARIANT: Only names listed in __all__ are importable via __getattr__. # #region Route_Group_Contracts [C:3] [TYPE Block] @@ -13,6 +15,8 @@ # @RELATION DEPENDS_ON -> [ConnectionsRouter] # @RELATION DEPENDS_ON -> [ReportsRouter] # @RELATION DEPENDS_ON -> [LlmRoutes] +# @SIDE_EFFECT: Registers route group imports via __getattr__ +# @DATA_CONTRACT: Package -> RouterModule mapping __all__ = [ "admin", "assistant", diff --git a/backend/src/api/routes/__tests__/test_assistant_authz.py b/backend/src/api/routes/__tests__/test_assistant_authz.py index 9cb37aff..3a36a644 100644 --- a/backend/src/api/routes/__tests__/test_assistant_authz.py +++ b/backend/src/api/routes/__tests__/test_assistant_authz.py @@ -3,7 +3,7 @@ import os os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w=" # #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac] # @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users. -# @LAYER: UI (API Tests) +# @LAYER: API # @RELATION DEPENDS_ON -> AssistantApi # @INVARIANT: Security-sensitive flows fail closed for unauthorized actors. import asyncio diff --git a/backend/src/api/routes/__tests__/test_git_status_route.py b/backend/src/api/routes/__tests__/test_git_status_route.py index 506c590d..1504df5e 100644 --- a/backend/src/api/routes/__tests__/test_git_status_route.py +++ b/backend/src/api/routes/__tests__/test_git_status_route.py @@ -1,6 +1,6 @@ # #region TestGitStatusRoute [TYPE Module] [C:3] [SEMANTICS tests, git, api, status, no_repo] # @BRIEF Validate status endpoint behavior for missing and error repository states. -# @LAYER: Domain (Tests) +# @LAYER: Domain # @RELATION VERIFIES -> [GitApi] import asyncio from unittest.mock import MagicMock diff --git a/backend/src/api/routes/__tests__/test_reports_api.py b/backend/src/api/routes/__tests__/test_reports_api.py index c2070188..cf396278 100644 --- a/backend/src/api/routes/__tests__/test_reports_api.py +++ b/backend/src/api/routes/__tests__/test_reports_api.py @@ -1,7 +1,7 @@ # #region TestReportsApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, contract, pagination, filtering] # @RELATION BELONGS_TO -> SrcRoot # @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior. -# @LAYER: Domain (Tests) +# @LAYER: Domain # @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}. from datetime import UTC, datetime, timedelta from types import SimpleNamespace diff --git a/backend/src/api/routes/__tests__/test_reports_detail_api.py b/backend/src/api/routes/__tests__/test_reports_detail_api.py index 3f387002..41873514 100644 --- a/backend/src/api/routes/__tests__/test_reports_detail_api.py +++ b/backend/src/api/routes/__tests__/test_reports_detail_api.py @@ -1,7 +1,7 @@ # #region TestReportsDetailApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, detail, diagnostics] # @RELATION BELONGS_TO -> SrcRoot # @BRIEF Contract tests for GET /api/reports/{report_id} detail endpoint behavior. -# @LAYER: Domain (Tests) +# @LAYER: Domain # @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts. from datetime import datetime, timedelta from types import SimpleNamespace diff --git a/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py b/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py index b7fc8619..eda1428d 100644 --- a/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py +++ b/backend/src/api/routes/__tests__/test_reports_openapi_conformance.py @@ -1,7 +1,7 @@ # #region TestReportsOpenapiConformance [TYPE Module] [C:3] [SEMANTICS tests, reports, openapi, conformance] # @RELATION BELONGS_TO -> SrcRoot # @BRIEF Validate implemented reports payload shape against OpenAPI-required top-level contract fields. -# @LAYER: Domain (Tests) +# @LAYER: Domain # @INVARIANT: List and detail payloads include required contract keys. from datetime import datetime from types import SimpleNamespace diff --git a/backend/src/api/routes/__tests__/test_tasks_logs.py b/backend/src/api/routes/__tests__/test_tasks_logs.py index a4e836d6..2fd79779 100644 --- a/backend/src/api/routes/__tests__/test_tasks_logs.py +++ b/backend/src/api/routes/__tests__/test_tasks_logs.py @@ -1,7 +1,7 @@ # #region test_tasks_logs_module [TYPE Module] [C:2] [SEMANTICS tests, tasks, logs, api, contract, validation] # @RELATION VERIFIES -> [src.api.routes.tasks:Module] # @BRIEF Contract testing for task logs API endpoints. -# @LAYER: Domain (Tests) +# @LAYER: Domain from unittest.mock import MagicMock import pytest diff --git a/backend/src/api/routes/admin.py b/backend/src/api/routes/admin.py index b2aa47d0..7d5d0ae6 100644 --- a/backend/src/api/routes/admin.py +++ b/backend/src/api/routes/admin.py @@ -1,7 +1,7 @@ -# #region AdminApi [C:3] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user] +# #region AdminApi [C:5] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user] # # @BRIEF Admin API endpoints for user and role management. -# @LAYER: API +# @LAYER: API # @RELATION DEPENDS_ON -> [AuthRepository:Class] # @RELATION DEPENDS_ON -> [get_auth_db:Function] # @RELATION DEPENDS_ON -> [has_permission:Function] diff --git a/backend/src/api/routes/assistant/_llm_planner.py b/backend/src/api/routes/assistant/_llm_planner.py index b065c368..b4e92ce4 100644 --- a/backend/src/api/routes/assistant/_llm_planner.py +++ b/backend/src/api/routes/assistant/_llm_planner.py @@ -1,10 +1,14 @@ -# #region AssistantLlmPlanner [C:3] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog] +# #region AssistantLlmPlanner [C:5] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog] # @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API. # @LAYER: API # @RELATION DEPENDS_ON -> [AssistantSchemas] # @RELATION DEPENDS_ON -> [AssistantResolvers] # @RELATION DISPATCHES -> [AssistantLlmPlannerIntent] +# @PRE: Assistant routes initialized, user authenticated +# @POST: LLM tool catalog filtered and returned # @INVARIANT: Tool catalog is filtered by user permissions before being sent to LLM. +# @SIDE_EFFECT: Filters tool catalog by user permissions +# @DATA_CONTRACT: UserPermissions -> ToolCatalog from __future__ import annotations diff --git a/backend/src/api/routes/assistant/_llm_planner_intent.py b/backend/src/api/routes/assistant/_llm_planner_intent.py index a4724b8a..6cc27151 100644 --- a/backend/src/api/routes/assistant/_llm_planner_intent.py +++ b/backend/src/api/routes/assistant/_llm_planner_intent.py @@ -1,9 +1,13 @@ -# #region AssistantLlmPlannerIntent [C:3] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization] +# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization] # @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog. # @LAYER: API # @RELATION DEPENDS_ON -> [AssistantLlmPlanner] # @RELATION DEPENDS_ON -> [AssistantResolvers] +# @PRE: Assistant routes initialized, user authenticated +# @POST: Intent planning registered with confirmation gate # @INVARIANT: Production deployments always require confirmation. +# @SIDE_EFFECT: Registers intent planning routes +# @DATA_CONTRACT: UserIntent -> PlannedAction from __future__ import annotations diff --git a/backend/src/api/routes/clean_release_v2.py b/backend/src/api/routes/clean_release_v2.py index 9a9a7feb..094ab91d 100644 --- a/backend/src/api/routes/clean_release_v2.py +++ b/backend/src/api/routes/clean_release_v2.py @@ -1,6 +1,6 @@ # #region CleanReleaseV2Api [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, candidate, lifecycle, api] # @BRIEF Redesigned clean release API for headless candidate lifecycle. -# @LAYER: UI (API) +# @LAYER: API # @RELATION DEPENDS_ON -> [CleanReleaseRepository] # @RELATION CALLS -> [approve_candidate] # @RELATION CALLS -> [publish_candidate] diff --git a/backend/src/api/routes/connections.py b/backend/src/api/routes/connections.py index 4be63c50..b6372ff5 100644 --- a/backend/src/api/routes/connections.py +++ b/backend/src/api/routes/connections.py @@ -1,6 +1,6 @@ # #region ConnectionsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, connection, api, search] # @BRIEF Defines the FastAPI router for managing external database connections. -# @LAYER: UI (API) +# @LAYER: API # @RELATION DEPENDS_ON -> [get_db] # @RELATION DEPENDS_ON -> [ConnectionConfig] diff --git a/backend/src/api/routes/datasets.py b/backend/src/api/routes/datasets.py index 08e1b14f..5d046218 100644 --- a/backend/src/api/routes/datasets.py +++ b/backend/src/api/routes/datasets.py @@ -1,4 +1,4 @@ -# #region DatasetsApi [C:3] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields] +# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields] # # @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress # @LAYER: API @@ -6,6 +6,10 @@ # @RELATION DEPENDS_ON -> [ResourceService] # @RELATION DEPENDS_ON -> [SupersetClient] # +# @PRE SupersetClient is available; env_id is valid. +# @POST Returns dataset metadata with mapping status. +# @SIDE_EFFECT Reads from Superset API and task manager. +# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse # @INVARIANT: All dataset responses include last_task metadata @@ -102,7 +106,7 @@ class TaskResponse(BaseModel): task_id: str # #endregion TaskResponse -# #region get_dataset_ids [C:3] [TYPE Function] +# #region get_dataset_ids [C:4] [TYPE Function] # @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination) # @PRE: env_id must be a valid environment ID # @POST: Returns a list of all dataset IDs @@ -150,7 +154,7 @@ async def get_dataset_ids( raise HTTPException(status_code=503, detail=f"Failed to fetch dataset IDs: {e!s}") # #endregion get_dataset_ids -# #region get_datasets [C:3] [TYPE Function] +# #region get_datasets [C:4] [TYPE Function] # @BRIEF Fetch list of datasets from a specific environment with mapping progress # @PRE: env_id must be a valid environment ID # @PRE: page must be >= 1 if provided @@ -234,7 +238,7 @@ class MapColumnsRequest(BaseModel): file_data: str | None = Field(None, description="File path or data for XLSX source") # #endregion MapColumnsRequest -# #region map_columns [C:3] [TYPE Function] +# #region map_columns [C:4] [TYPE Function] # @BRIEF Trigger bulk column mapping for datasets # @PRE: User has permission plugin:mapper:execute # @PRE: env_id is a valid environment ID @@ -302,7 +306,7 @@ class GenerateDocsRequest(BaseModel): options: dict | None = Field(None, description="Additional options for documentation generation") # #endregion GenerateDocsRequest -# #region generate_docs [C:3] [TYPE Function] +# #region generate_docs [C:4] [TYPE Function] # @BRIEF Trigger bulk documentation generation for datasets # @PRE: User has permission plugin:llm_analysis:execute # @PRE: env_id is a valid environment ID @@ -355,7 +359,7 @@ async def generate_docs( raise HTTPException(status_code=503, detail=f"Failed to create documentation generation task: {e!s}") # #endregion generate_docs -# #region get_dataset_detail [C:3] [TYPE Function] +# #region get_dataset_detail [C:4] [TYPE Function] # @BRIEF Get detailed dataset information including columns and linked dashboards # @PRE: env_id is a valid environment ID # @PRE: dataset_id is a valid dataset ID @@ -381,6 +385,11 @@ async def get_dataset_detail( client = SupersetClient(env) dataset_detail = client.get_dataset_detail(dataset_id) + # Normalize 'database' field: Superset returns an object, model expects a string + if isinstance(dataset_detail.get("database"), dict): + db_obj = dataset_detail["database"] + dataset_detail["database"] = db_obj.get("database_name", str(db_obj.get("id", ""))) + logger.info(f"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards") return DatasetDetailResponse(**dataset_detail) diff --git a/backend/src/api/routes/environments.py b/backend/src/api/routes/environments.py index 2186fb72..67e26ae6 100644 --- a/backend/src/api/routes/environments.py +++ b/backend/src/api/routes/environments.py @@ -1,7 +1,7 @@ -# #region EnvironmentsApi [C:3] [TYPE Module] [SEMANTICS fastapi, environment, api] +# #region EnvironmentsApi [C:5] [TYPE Module] [SEMANTICS fastapi, environment, api] # # @BRIEF API endpoints for listing environments and their databases. -# @LAYER: API +# @LAYER: API # @RELATION DEPENDS_ON -> [AppDependencies] # @RELATION DEPENDS_ON -> [SupersetClient] # diff --git a/backend/src/api/routes/git/__init__.py b/backend/src/api/routes/git/__init__.py index 34ca4726..cf4beb04 100644 --- a/backend/src/api/routes/git/__init__.py +++ b/backend/src/api/routes/git/__init__.py @@ -1,10 +1,14 @@ -# #region GitPackage [C:3] [TYPE Module] [SEMANTICS git, api, package, sync] +# #region GitPackage [C:5] [TYPE Module] [SEMANTICS git, api, package, sync] # @BRIEF Package root for decomposed git routes. Re-exports all public symbols from submodules. # @LAYER: API # @RELATION USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes, # GitRepoRoutes, GitRepoOperationsRoutes, GitRepoLifecycleRoutes, # GitMergeRoutes, GitEnvironmentRoutes] # @INVARIANT: git_service and os are module-level attributes for test monkeypatch compatibility. +# @PRE: Git service initialized +# @POST: Git API package exported +# @SIDE_EFFECT: Registers git route submodules +# @DATA_CONTRACT: GitRequest -> GitResponse # All route functions are re-exported for direct access via `from src.api.routes import git`. import os diff --git a/backend/src/api/routes/llm.py b/backend/src/api/routes/llm.py index 3ce149f1..80b6cb10 100644 --- a/backend/src/api/routes/llm.py +++ b/backend/src/api/routes/llm.py @@ -1,6 +1,6 @@ # #region LlmRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, llm, api, provider] # @BRIEF API routes for LLM provider configuration and management. -# @LAYER: UI (API) +# @LAYER: API # @RELATION DEPENDS_ON -> [LLMProviderService] # @RELATION DEPENDS_ON -> [LLMProviderConfig] # @RELATION DEPENDS_ON -> [get_current_user] @@ -30,10 +30,12 @@ class FetchModelsRequest(BaseModel): # #endregion FetchModelsRequest -# #region router [TYPE Global] +# #region router [C:1] [TYPE Global] # @BRIEF APIRouter instance for LLM routes. + router = APIRouter(tags=["LLM"]) # #endregion router +# #endregion router # #region _is_valid_runtime_api_key [C:4] [TYPE Function] diff --git a/backend/src/api/routes/plugins.py b/backend/src/api/routes/plugins.py index 8f5c0107..316a73dc 100755 --- a/backend/src/api/routes/plugins.py +++ b/backend/src/api/routes/plugins.py @@ -1,6 +1,6 @@ # #region PluginsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, plugin, api] # @BRIEF Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins. -# @LAYER: UI (API) +# @LAYER: API # @RELATION DEPENDS_ON -> [PluginConfig] # @RELATION DEPENDS_ON -> [get_plugin_loader] # @RELATION BINDS_TO -> [API_Routes] diff --git a/backend/src/api/routes/profile.py b/backend/src/api/routes/profile.py index 5669024e..d616b5c0 100644 --- a/backend/src/api/routes/profile.py +++ b/backend/src/api/routes/profile.py @@ -1,4 +1,4 @@ -# #region ProfileApiModule [C:3] [TYPE Module] [SEMANTICS fastapi, profile, api, validate, search, superset] +# #region ProfileApiModule [C:5] [TYPE Module] [SEMANTICS fastapi, profile, api, validate, search, superset] # # @BRIEF Exposes self-scoped profile preference endpoints and environment-based Superset account lookup. # @LAYER: API @@ -7,11 +7,15 @@ # @RELATION DEPENDS_ON -> [get_db] # # @INVARIANT: Endpoints are self-scoped and never mutate another user preference. +# @PRE: Auth middleware configured, database session available +# @POST: Profile endpoints registered # @UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user. # @UX_STATE: Saving -> Validation errors map to actionable 422 details. # @UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload. # @UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback. # @UX_RECOVERY: Lookup degradation keeps manual username save path available. +# @SIDE_EFFECT: Registers /api/profile/* routes +# @DATA_CONTRACT: ProfileRequest -> ProfileResponse from fastapi import APIRouter, Depends, HTTPException, Query diff --git a/backend/src/api/routes/reports.py b/backend/src/api/routes/reports.py index af17e900..b37f875e 100644 --- a/backend/src/api/routes/reports.py +++ b/backend/src/api/routes/reports.py @@ -1,6 +1,6 @@ # #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, report, api, transform, search, task] # @BRIEF FastAPI router for unified task report list and detail retrieval endpoints. -# @LAYER: UI (API) +# @LAYER: API # @RELATION DEPENDS_ON -> [ReportsService:Class] # @RELATION DEPENDS_ON -> [get_task_manager:Function] # @RELATION DEPENDS_ON -> [get_clean_release_repository:Function] diff --git a/backend/src/api/routes/settings.py b/backend/src/api/routes/settings.py index 8a665011..dd2dcdc6 100755 --- a/backend/src/api/routes/settings.py +++ b/backend/src/api/routes/settings.py @@ -1,4 +1,4 @@ -# #region SettingsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response] +# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response] # # @BRIEF Provides API endpoints for managing application settings and Superset environments. # @LAYER: API @@ -6,6 +6,10 @@ # @RELATION DEPENDS_ON -> [get_config_manager] # @RELATION DEPENDS_ON -> [has_permission] # +# @PRE ConfigManager is initialized and accessible. +# @POST Settings are read or written via ConfigManager. +# @SIDE_EFFECT Persists config changes to disk via ConfigManager. +# @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse # @INVARIANT: All settings changes must be persisted via ConfigManager. # @PUBLIC_API: router diff --git a/backend/src/api/routes/storage.py b/backend/src/api/routes/storage.py index b3e91b38..d91f8a39 100644 --- a/backend/src/api/routes/storage.py +++ b/backend/src/api/routes/storage.py @@ -1,7 +1,7 @@ -# #region storage_routes [C:3] [TYPE Module] [SEMANTICS fastapi, storage, api, upload, download] +# #region storage_routes [C:5] [TYPE Module] [SEMANTICS fastapi, storage, api, upload, download] # # @BRIEF API endpoints for file storage management (backups and repositories). -# @LAYER: API +# @LAYER: API # @RELATION DEPENDS_ON -> [StorageModels] # @RELATION DEPENDS_ON -> [StoragePlugin] # diff --git a/backend/src/api/routes/tasks.py b/backend/src/api/routes/tasks.py index 53cf9b8b..81184a3a 100755 --- a/backend/src/api/routes/tasks.py +++ b/backend/src/api/routes/tasks.py @@ -1,6 +1,6 @@ # #region TasksRouter [C:3] [TYPE Module] [SEMANTICS fastapi, task, api, search, create-task-request, resolve-task-request] # @BRIEF Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks. -# @LAYER: UI (API) +# @LAYER: API # @RELATION DEPENDS_ON -> [TaskManager] # @RELATION DEPENDS_ON -> [ConfigManager] # @RELATION DEPENDS_ON -> [LLMProviderService] diff --git a/backend/src/app.py b/backend/src/app.py index 5729e513..a705e4cb 100755 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,6 +1,6 @@ # #region AppModule [C:5] [TYPE Module] [SEMANTICS fastapi, websocket, startup, scheduler, middleware] -# @BRIEF The main entry point for the FastAPI application. It initializes the app, configures CORS, sets up dependencies, includes API routers, and defines the WebSocket endpoint for log streaming. -# @LAYER UI (API) +# @BRIEF The main entry point for the FastAPI application. +# @LAYER API # @RELATION DEPENDS_ON -> [ApiRoutesModule] # @RELATION DEPENDS_ON -> [ApiRoutesModule] # @INVARIANT All WebSocket connections must be properly cleaned up on disconnect. diff --git a/backend/src/core/auth/jwt.py b/backend/src/core/auth/jwt.py index 147d3dcb..03783ff9 100644 --- a/backend/src/core/auth/jwt.py +++ b/backend/src/core/auth/jwt.py @@ -1,11 +1,15 @@ -# #region AuthJwtModule [C:3] [TYPE Module] [SEMANTICS auth, validate, jwt, token] +# #region AuthJwtModule [C:5] [TYPE Module] [SEMANTICS auth, validate, jwt, token] # # @BRIEF JWT token generation and validation logic. -# @LAYER: Core +# @LAYER: Core # @RELATION DEPENDS_ON -> [auth_config] # @RELATION USES -> [auth_config] # # @INVARIANT: Tokens must include expiration time and user identifier. +# @PRE: JWT secret configured in environment +# @POST: Token encode/decode functions exported +# @SIDE_EFFECT: None +# @DATA_CONTRACT: TokenPayload -> JWT string from datetime import datetime, timedelta diff --git a/backend/src/core/auth/logger.py b/backend/src/core/auth/logger.py index a4de434d..7f627bdd 100644 --- a/backend/src/core/auth/logger.py +++ b/backend/src/core/auth/logger.py @@ -1,10 +1,16 @@ -# #region AuthLoggerModule [C:3] [TYPE Module] [SEMANTICS auth, audit, logging] +# #region AuthLoggerModule [C:5] [TYPE Module] [SEMANTICS auth, audit, logging] # -# @BRIEF Audit logging for security-related events. -# @LAYER: Core -# @RELATION USES -> belief_scope +# @BRIEF Structured auth logging module for audit trail generation. +# @LAYER: Core +# @RELATION DEPENDS_ON -> [core_logger] # # @INVARIANT: Must not log sensitive data like passwords or full tokens. +# @PRE: Auth module initialized +# @POST: Audit logging functions exported +# @SIDE_EFFECT: Writes auth audit log entries +# @DATA_CONTRACT: AuthEvent -> LogEntry +# @SIDE_EFFECT: Writes auth audit log entries +# @DATA_CONTRACT: AuthEvent -> LogEntry from datetime import datetime diff --git a/backend/src/core/database.py b/backend/src/core/database.py index a564ea3b..29157ab4 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -1,7 +1,7 @@ # #region DatabaseModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, connection, session] # # @BRIEF Configures database connection and session management (PostgreSQL-first). -# @LAYER: Core +# @LAYER: Infrastructure # @RELATION DEPENDS_ON -> [MappingModels] # @RELATION DEPENDS_ON -> [auth_config] # @RELATION DEPENDS_ON -> [ConnectionConfig] diff --git a/backend/src/core/migration/archive_parser.py b/backend/src/core/migration/archive_parser.py index d0b0493c..7202e4a4 100644 --- a/backend/src/core/migration/archive_parser.py +++ b/backend/src/core/migration/archive_parser.py @@ -1,8 +1,12 @@ -# #region MigrationArchiveParserModule [C:3] [TYPE Module] [SEMANTICS migration, transform, superset, migration-archive-parser] +# #region MigrationArchiveParserModule [C:5] [TYPE Module] [SEMANTICS migration, transform, superset, migration-archive-parser] # @BRIEF Parse Superset export ZIP archives into normalized object catalogs for diffing. -# @LAYER: Core +# @LAYER: Domain # @RELATION DEPENDS_ON -> [LoggerModule] # @INVARIANT: Parsing is read-only and never mutates archive files. +# @PRE: Archive file path is valid and readable +# @POST: Parsed migration archive returned +# @SIDE_EFFECT: Reads archive file (read-only) +# @DATA_CONTRACT: ArchivePath -> ParsedMigration import json import tempfile import zipfile diff --git a/backend/src/core/migration/dry_run_orchestrator.py b/backend/src/core/migration/dry_run_orchestrator.py index f7039fc9..0d5c5eda 100644 --- a/backend/src/core/migration/dry_run_orchestrator.py +++ b/backend/src/core/migration/dry_run_orchestrator.py @@ -1,11 +1,15 @@ -# #region MigrationDryRunOrchestratorModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, migration, orchestration, diff, migration-dry-run-service] +# #region MigrationDryRunOrchestratorModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, migration, orchestration, diff, migration-dry-run-service] # @BRIEF Compute pre-flight migration diff and risk scoring without apply. -# @LAYER: Core +# @LAYER: Domain # @RELATION DEPENDS_ON -> [SupersetClient] # @RELATION DEPENDS_ON -> [MigrationEngine] # @RELATION DEPENDS_ON -> [MigrationArchiveParser] # @RELATION DEPENDS_ON -> [RiskAssessorModule] # @INVARIANT: Dry run is informative only and must not mutate target environment. +# @PRE: Source and target environments configured +# @POST: Dry-run diff returned without mutation +# @SIDE_EFFECT: Reads source environment (read-only) +# @DATA_CONTRACT: EnvironmentConfig -> MigrationDiffReport import json from datetime import UTC, datetime from typing import Any diff --git a/backend/src/core/superset_client/__init__.py b/backend/src/core/superset_client/__init__.py index 0ccf4c4b..8f468a11 100644 --- a/backend/src/core/superset_client/__init__.py +++ b/backend/src/core/superset_client/__init__.py @@ -1,5 +1,5 @@ -# #region SupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, package, superset-client] -# @LAYER: Infra +# #region SupersetClientModule [C:5] [TYPE Module] [SEMANTICS superset, package, superset-client] +# @LAYER: Service # @BRIEF Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию. # @RELATION DEPENDS_ON -> [ConfigModels] # @RELATION DEPENDS_ON -> [APIClient] @@ -15,6 +15,10 @@ # preserves the original public API surface — all consumers continue to import # `from src.core.superset_client import SupersetClient` without changes. # @REJECTED: Keeping a single 2145-line file — violates fractal limit INV_7. +# @PRE: Superset instance URL and credentials configured +# @POST: SupersetClient class exported +# @SIDE_EFFECT: Establishes HTTP connection pool to Superset +# @DATA_CONTRACT: SupersetConfig -> SupersetClient from ._base import SupersetClientBase from ._charts import SupersetChartsMixin diff --git a/backend/src/core/superset_profile_lookup.py b/backend/src/core/superset_profile_lookup.py index 7b63405f..b474834e 100644 --- a/backend/src/core/superset_profile_lookup.py +++ b/backend/src/core/superset_profile_lookup.py @@ -1,12 +1,14 @@ -# #region SupersetProfileLookup [C:3] [TYPE Module] [SEMANTICS superset, profile, account, lookup, adapter] +# #region SupersetProfileLookup [C:5] [TYPE Module] [SEMANTICS superset, profile, account, lookup, adapter] # # @BRIEF Provides environment-scoped Superset account lookup adapter with stable normalized output. -# @LAYER: Core +# @LAYER: Service # @RELATION DEPENDS_ON -> [APIClient] # @RELATION DEPENDS_ON -> [SupersetAPIError] # @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter] # # @INVARIANT: Adapter never leaks raw upstream payload shape to API consumers. +# @SIDE_EFFECT: Makes HTTP requests to Superset +# @DATA_CONTRACT: ProfileQuery -> SupersetProfile import json from typing import Any diff --git a/backend/src/core/task_manager/event_bus.py b/backend/src/core/task_manager/event_bus.py index f8fdfe93..135e47ae 100644 --- a/backend/src/core/task_manager/event_bus.py +++ b/backend/src/core/task_manager/event_bus.py @@ -17,6 +17,7 @@ # into subscriber queues, runs background flusher thread. # @INVARIANT Buffered logs are retried on persistence failure; every subscriber receives only # task-scoped events. +# @DATA_CONTRACT Input: LogEntry -> Output: persisted log + subscriber notification import asyncio import threading diff --git a/backend/src/core/task_manager/models.py b/backend/src/core/task_manager/models.py index d2c77364..9cd08658 100644 --- a/backend/src/core/task_manager/models.py +++ b/backend/src/core/task_manager/models.py @@ -1,10 +1,14 @@ -# #region TaskManagerModels [C:3] [TYPE Module] [SEMANTICS pydantic, task, model, validate, task-status] +# #region TaskManagerModels [C:5] [TYPE Module] [SEMANTICS pydantic, task, model, validate, task-status] # @BRIEF Defines the data models and enumerations used by the Task Manager. -# @LAYER: Core +# @LAYER: Domain # @RELATION USED_BY -> [TaskManager] # @RELATION USED_BY -> [TaskManagerPackage] # @INVARIANT: Task IDs are immutable once created. # @CONSTRAINT: Must use Pydantic for data validation. +# @PRE: Task manager initialized +# @POST: Task models exported with immutable IDs +# @SIDE_EFFECT: Defines task data schema +# @DATA_CONTRACT: TaskInput -> TaskModel import uuid from datetime import datetime from enum import Enum diff --git a/backend/src/models/assistant.py b/backend/src/models/assistant.py index ad59e78e..588a275e 100644 --- a/backend/src/models/assistant.py +++ b/backend/src/models/assistant.py @@ -3,6 +3,8 @@ # @LAYER: Domain # @RELATION DEPENDS_ON -> MappingModels # @INVARIANT: Assistant records preserve immutable ids and creation timestamps. +# @SIDE_EFFECT: Defines assistant audit/message/confirmation tables +# @DATA_CONTRACT: AssistantData -> AssistantRecord from datetime import datetime diff --git a/backend/src/models/auth.py b/backend/src/models/auth.py index 9dd550f0..fed56217 100644 --- a/backend/src/models/auth.py +++ b/backend/src/models/auth.py @@ -1,9 +1,13 @@ -# #region AuthModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, auth, model, schema, user] +# #region AuthModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, model, schema, user] # @BRIEF SQLAlchemy models for multi-user authentication and authorization. -# @LAYER: Domain +# @LAYER: Domain # @RELATION INHERITS_FROM -> [Base] # # @INVARIANT: Usernames and emails must be unique. +# @PRE: Database engine initialized +# @POST: Auth ORM models registered with unique constraints +# @SIDE_EFFECT: Defines auth user tables +# @DATA_CONTRACT: UserData -> UserRecord import uuid from datetime import datetime diff --git a/backend/src/models/dataset_review_pkg/_clarification_models.py b/backend/src/models/dataset_review_pkg/_clarification_models.py index e68d419d..bf9c10bc 100644 --- a/backend/src/models/dataset_review_pkg/_clarification_models.py +++ b/backend/src/models/dataset_review_pkg/_clarification_models.py @@ -4,6 +4,10 @@ # @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] # @RELATION DEPENDS_ON -> [MappingModels] # @INVARIANT: Only one active clarification question may exist at a time per session. +# @PRE: Database engine initialized +# @POST: Clarification ORM models registered +# @SIDE_EFFECT: Defines dataset review clarification tables +# @DATA_CONTRACT: ClarificationData -> ClarificationRecord import uuid from datetime import datetime diff --git a/backend/src/models/dataset_review_pkg/_semantic_models.py b/backend/src/models/dataset_review_pkg/_semantic_models.py index ed16fc51..e8bfe5e1 100644 --- a/backend/src/models/dataset_review_pkg/_semantic_models.py +++ b/backend/src/models/dataset_review_pkg/_semantic_models.py @@ -4,6 +4,10 @@ # @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] # @RELATION DEPENDS_ON -> [MappingModels] # @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values. +# @PRE: Database engine initialized +# @POST: Semantic ORM models registered with override protection +# @SIDE_EFFECT: Defines dataset review semantic tables +# @DATA_CONTRACT: SemanticData -> SemanticFieldEntry import uuid from datetime import datetime diff --git a/backend/src/models/dataset_review_pkg/_session_models.py b/backend/src/models/dataset_review_pkg/_session_models.py index f1e5e00e..63ed8fbd 100644 --- a/backend/src/models/dataset_review_pkg/_session_models.py +++ b/backend/src/models/dataset_review_pkg/_session_models.py @@ -4,6 +4,10 @@ # @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module] # @RELATION DEPENDS_ON -> [MappingModels] # @INVARIANT: Session and profile entities are strictly scoped to an authenticated user. +# @PRE: Database engine initialized +# @POST: Session ORM models registered with optimistic locking +# @SIDE_EFFECT: Defines dataset review session tables +# @DATA_CONTRACT: SessionData -> SessionRecord import uuid from datetime import datetime diff --git a/backend/src/models/mapping.py b/backend/src/models/mapping.py index 4051affe..80d1fa3a 100644 --- a/backend/src/models/mapping.py +++ b/backend/src/models/mapping.py @@ -1,12 +1,16 @@ -# #region MappingModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, mapping, model, schema, resource-type] +# #region MappingModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, mapping, model, schema, resource-type] # # @BRIEF Defines the database schema for environment metadata and database mappings using SQLAlchemy. -# @LAYER: Domain +# @LAYER: Domain # @RELATION DEPENDS_ON -> sqlalchemy # # @INVARIANT: All primary keys are UUID strings. # @CONSTRAINT: source_env_id and target_env_id must be valid environment IDs. +# @PRE: Database engine initialized +# @POST: Mapping ORM models registered with UUID primary keys +# @SIDE_EFFECT: Defines environment/database/resource mapping tables +# @DATA_CONTRACT: MappingData -> MappingRecord import enum import uuid diff --git a/backend/src/models/profile.py b/backend/src/models/profile.py index c98467ca..5a99a1bd 100644 --- a/backend/src/models/profile.py +++ b/backend/src/models/profile.py @@ -1,12 +1,16 @@ -# #region ProfileModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, profile, model, schema, git, dashboard] +# #region ProfileModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, model, schema, git, dashboard] # # @BRIEF Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences. -# @LAYER: Domain +# @LAYER: Domain # @RELATION DEPENDS_ON -> [AuthModels] # @RELATION INHERITS_FROM -> [MappingModels:Base] # # @INVARIANT: Exactly one preference row exists per user_id. # @INVARIANT: Sensitive Git token is stored encrypted and never returned in plaintext. +# @PRE: Database engine initialized +# @POST: Profile ORM models registered +# @SIDE_EFFECT: Defines user profile/preference tables +# @DATA_CONTRACT: ProfileData -> PreferenceRecord import uuid from datetime import datetime diff --git a/backend/src/plugins/llm_analysis/plugin.py b/backend/src/plugins/llm_analysis/plugin.py index 68f4394a..8f4ce6cd 100644 --- a/backend/src/plugins/llm_analysis/plugin.py +++ b/backend/src/plugins/llm_analysis/plugin.py @@ -1,12 +1,13 @@ -# #region LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation] +# #region LLMAnalysisPlugin [C:5] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation] # @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin. -# @LAYER: Domain +# @LAYER: Plugin # @RELATION INHERITS -> [PluginBase] # @RELATION CALLS -> [ScreenshotService] # @RELATION CALLS -> [LLMClient] # @RELATION CALLS -> [LLMProviderService] # @RELATION USES -> TaskContext # @INVARIANT: All LLM interactions must be executed as asynchronous tasks. +# @DATA_CONTRACT: AnalysisRequest -> AnalysisResult import json import os diff --git a/backend/src/plugins/llm_analysis/service.py b/backend/src/plugins/llm_analysis/service.py index 6421dae0..18c70705 100644 --- a/backend/src/plugins/llm_analysis/service.py +++ b/backend/src/plugins/llm_analysis/service.py @@ -1,10 +1,11 @@ -# #region LLMAnalysisService [C:3] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity] +# #region LLMAnalysisService [C:5] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity] # @BRIEF Services for LLM interaction and dashboard screenshots. -# @LAYER: Domain +# @LAYER: Plugin # @RELATION DEPENDS_ON -> playwright # @RELATION DEPENDS_ON -> openai # @RELATION DEPENDS_ON -> tenacity # @INVARIANT: Screenshots must be 1920px width and capture full page height. +# @DATA_CONTRACT: DashboardSpec -> Screenshot + Analysis import asyncio import base64 diff --git a/backend/src/plugins/translate/__tests__/test_dictionary_crud.py b/backend/src/plugins/translate/__tests__/test_dictionary_crud.py index e9ba4678..37e8b108 100644 --- a/backend/src/plugins/translate/__tests__/test_dictionary_crud.py +++ b/backend/src/plugins/translate/__tests__/test_dictionary_crud.py @@ -215,4 +215,4 @@ class TestDictionaryEntryCRUD: entries, total = DictionaryManager.list_entries(db_session, d.id) assert total == 2 # endregion test_same_term_different_language_pair -# #endregion TestDictionaryCrud +# #endregion TestDictionaryCRUD diff --git a/backend/src/plugins/translate/__tests__/test_inline_correction.py b/backend/src/plugins/translate/__tests__/test_inline_correction.py index 5648e8d1..118cf4e9 100644 --- a/backend/src/plugins/translate/__tests__/test_inline_correction.py +++ b/backend/src/plugins/translate/__tests__/test_inline_correction.py @@ -1,7 +1,7 @@ # region InlineCorrectionTests [TYPE Module] # @SEMANTICS: test, translate, correction, inline, bulk # @PURPOSE: Tests for inline correction (T109-T116, T117): single correction, dictionary submission, bulk replace. -# @LAYER: Test +# @LAYER: Domain # @RELATION: BINDS_TO -> [InlineCorrectionService:Module] # @RELATION: BINDS_TO -> [BulkFindReplaceService:Module] # @TEST_CONTRACT: InlineCorrectionService -> submit_correction, duplicate detection diff --git a/backend/src/plugins/translate/__tests__/test_token_budget.py b/backend/src/plugins/translate/__tests__/test_token_budget.py index d0875118..e01d5b41 100644 --- a/backend/src/plugins/translate/__tests__/test_token_budget.py +++ b/backend/src/plugins/translate/__tests__/test_token_budget.py @@ -192,4 +192,4 @@ class TestTokenBudget: # endregion test_no_target_languages_defaults_to_en # endregion TestTokenBudget -# endregion TestTokenBudget +# #endregion TestTokenBudget diff --git a/backend/src/schemas/auth.py b/backend/src/schemas/auth.py index 079f3679..06ef58ab 100644 --- a/backend/src/schemas/auth.py +++ b/backend/src/schemas/auth.py @@ -1,10 +1,11 @@ -# #region AuthSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, auth, schema, token] +# #region AuthSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, auth, schema, token] # # @BRIEF Pydantic schemas for authentication requests and responses. -# @LAYER: API +# @LAYER: API # @RELATION DEPENDS_ON -> pydantic # # @INVARIANT: Sensitive fields like password must not be included in response schemas. +# @DATA_CONTRACT: AuthPayload -> AuthSchema from datetime import datetime diff --git a/backend/src/schemas/profile.py b/backend/src/schemas/profile.py index ec89dd35..fd4e84ae 100644 --- a/backend/src/schemas/profile.py +++ b/backend/src/schemas/profile.py @@ -1,10 +1,11 @@ -# #region ProfileSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, profile, schema, superset, profile-permission-state] +# #region ProfileSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, profile, schema, superset, profile-permission-state] # # @BRIEF Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup. -# @LAYER: API +# @LAYER: API # @RELATION DEPENDS_ON -> pydantic # # @INVARIANT: Schema shapes stay stable for profile UI states and backend preference contracts. +# @DATA_CONTRACT: ProfilePayload -> ProfileSchema from datetime import datetime from typing import Literal diff --git a/backend/src/scripts/clean_release_tui.py b/backend/src/scripts/clean_release_tui.py index b1c10596..0850d692 100644 --- a/backend/src/scripts/clean_release_tui.py +++ b/backend/src/scripts/clean_release_tui.py @@ -4,6 +4,7 @@ # @RELATION DEPENDS_ON -> [ComplianceExecutionService] # @RELATION DEPENDS_ON -> [CleanReleaseRepository] # @INVARIANT: TUI refuses startup in non-TTY environments; headless flow is CLI/API only. +# @DATA_CONTRACT: CLIArgs -> TUIExitCode import contextlib import curses import json diff --git a/backend/src/scripts/create_admin.py b/backend/src/scripts/create_admin.py index 1847e116..f5a2066a 100644 --- a/backend/src/scripts/create_admin.py +++ b/backend/src/scripts/create_admin.py @@ -1,12 +1,14 @@ -# #region CreateAdminScript [C:3] [TYPE Module] [SEMANTICS admin, search, user, cli] +# #region CreateAdminScript [C:5] [TYPE Module] [SEMANTICS admin, search, user, cli] # # @BRIEF CLI tool for creating the initial admin user. -# @LAYER: Scripts +# @LAYER: Infrastructure # @RELATION USES -> [AuthSecurityModule] # @RELATION USES -> [DatabaseModule] # @RELATION USES -> [AuthModels] # # @INVARIANT: Admin user must have the "Admin" role. +# @SIDE_EFFECT: Writes admin user to database +# @DATA_CONTRACT: CLIArgs -> AdminUser import argparse import sys diff --git a/backend/src/scripts/seed_permissions.py b/backend/src/scripts/seed_permissions.py index 0fd9d2c2..d535a699 100644 --- a/backend/src/scripts/seed_permissions.py +++ b/backend/src/scripts/seed_permissions.py @@ -1,13 +1,15 @@ -# #region SeedPermissionsScript [C:3] [TYPE Module] [SEMANTICS rbac, search, auth] +# #region SeedPermissionsScript [C:5] [TYPE Module] [SEMANTICS rbac, search, auth] # # @BRIEF Populates the auth database with initial system permissions. -# @LAYER: Scripts +# @LAYER: Infrastructure # @RELATION DEPENDS_ON -> AuthSessionLocal # @RELATION DEPENDS_ON -> Permission # @RELATION DEPENDS_ON -> Role # @RELATION DEPENDS_ON -> AuthRepository # # @INVARIANT: Safe to run multiple times (idempotent). +# @SIDE_EFFECT: Writes permissions to database +# @DATA_CONTRACT: CLIArgs -> SeedSummary import sys from pathlib import Path diff --git a/backend/src/scripts/seed_superset_load_test.py b/backend/src/scripts/seed_superset_load_test.py index b9607017..882037bd 100644 --- a/backend/src/scripts/seed_superset_load_test.py +++ b/backend/src/scripts/seed_superset_load_test.py @@ -1,10 +1,11 @@ -# #region SeedSupersetLoadTestScript [C:3] [TYPE Module] [SEMANTICS superset, validate] +# #region SeedSupersetLoadTestScript [C:5] [TYPE Module] [SEMANTICS superset, validate] # # @BRIEF Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments. -# @LAYER: Scripts +# @LAYER: Infrastructure # @RELATION USES -> [ConfigManager] # @RELATION USES -> [SupersetClient] # @INVARIANT: Created chart and dashboard names are globally unique for one script run. +# @DATA_CONTRACT: CLIArgs -> TestDataSummary import argparse import json diff --git a/backend/src/services/clean_release/approval_service.py b/backend/src/services/clean_release/approval_service.py index ea3537a0..d62a664c 100644 --- a/backend/src/services/clean_release/approval_service.py +++ b/backend/src/services/clean_release/approval_service.py @@ -5,6 +5,10 @@ # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @RELATION DEPENDS_ON -> [AuditService] # @INVARIANT: Approval is allowed only for PASSED report bound to candidate; decisions are append-only. +# @PRE: Report with PASSED final_status exists for candidate +# @POST: Approval decision appended; candidate lifecycle advanced +# @SIDE_EFFECT: Persists approval decisions, transitions candidate status +# @DATA_CONTRACT: ApprovalRequest -> ApprovalDecision from __future__ import annotations @@ -207,4 +211,4 @@ def reject_candidate( # #endregion reject_candidate -# #endregion backend.src.services.clean_release.approval_service +# #endregion ApprovalService diff --git a/backend/src/services/clean_release/artifact_catalog_loader.py b/backend/src/services/clean_release/artifact_catalog_loader.py index 2aa41a15..5ea62f48 100644 --- a/backend/src/services/clean_release/artifact_catalog_loader.py +++ b/backend/src/services/clean_release/artifact_catalog_loader.py @@ -1,8 +1,10 @@ -# #region ArtifactCatalogLoader [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, artifact, catalog, manifest] +# #region ArtifactCatalogLoader [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, artifact, catalog, manifest] # @BRIEF Load bootstrap artifact catalogs for clean release real-mode flows. # @LAYER: Domain # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @INVARIANT: Artifact catalog must produce deterministic CandidateArtifact entries with required identity and checksum fields. +# @SIDE_EFFECT: Reads JSON file from filesystem +# @DATA_CONTRACT: FilePath -> CandidateArtifact[] from __future__ import annotations @@ -101,4 +103,4 @@ def load_bootstrap_artifacts(path: str, candidate_id: str) -> list[CandidateArti # #endregion load_bootstrap_artifacts -# #endregion backend.src.services.clean_release.artifact_catalog_loader +# #endregion ArtifactCatalogLoader diff --git a/backend/src/services/clean_release/audit_service.py b/backend/src/services/clean_release/audit_service.py index 3aa18e4a..fe439534 100644 --- a/backend/src/services/clean_release/audit_service.py +++ b/backend/src/services/clean_release/audit_service.py @@ -1,8 +1,12 @@ # #region AuditService [C:3] [TYPE Module] [SEMANTICS clean-release, audit, report, trail, release] # @BRIEF Provide lightweight audit hooks for clean release preparation/check/report lifecycle. -# @LAYER: Infra +# @LAYER: Infrastructure # @RELATION DEPENDS_ON -> [LoggerModule] # @INVARIANT: Audit hooks are append-only log actions. +# @PRE: Logger configured +# @POST: Audit events appended to log +# @SIDE_EFFECT: Writes audit events to logger and repository +# @DATA_CONTRACT: AuditAction -> LogEntry from __future__ import annotations @@ -114,4 +118,4 @@ def audit_report( ) -# #endregion backend.src.services.clean_release.audit_service +# #endregion AuditService diff --git a/backend/src/services/clean_release/candidate_service.py b/backend/src/services/clean_release/candidate_service.py index 5bc7ffc6..083b0b00 100644 --- a/backend/src/services/clean_release/candidate_service.py +++ b/backend/src/services/clean_release/candidate_service.py @@ -103,4 +103,4 @@ def register_candidate( return candidate # #endregion register_candidate -# #endregion backend.src.services.clean_release.candidate_service +# #endregion candidate_service diff --git a/backend/src/services/clean_release/demo_data_service.py b/backend/src/services/clean_release/demo_data_service.py index e1723746..d314aef1 100644 --- a/backend/src/services/clean_release/demo_data_service.py +++ b/backend/src/services/clean_release/demo_data_service.py @@ -1,8 +1,10 @@ -# #region DemoDataService [C:3] [TYPE Module] [SEMANTICS clean-release, demo, seed, fixture] +# #region DemoDataService [C:5] [TYPE Module] [SEMANTICS clean-release, demo, seed, fixture] # @BRIEF Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes. # @LAYER: Domain # @RELATION DEPENDS_ON -> [RepositoryRelations] # @INVARIANT: Demo and real namespaces must never collide for generated physical identifiers. +# @SIDE_EFFECT: Writes demo entities to repository +# @DATA_CONTRACT: SeedConfig -> DemoEntities from __future__ import annotations diff --git a/backend/src/services/clean_release/manifest_builder.py b/backend/src/services/clean_release/manifest_builder.py index cfd03ddb..8594dc11 100644 --- a/backend/src/services/clean_release/manifest_builder.py +++ b/backend/src/services/clean_release/manifest_builder.py @@ -1,8 +1,10 @@ -# #region ManifestBuilder [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, build, artifact, catalog] +# #region ManifestBuilder [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, build, artifact, catalog] # @BRIEF Build deterministic distribution manifest from classified artifact input. # @LAYER: Domain # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @INVARIANT: Equal semantic artifact sets produce identical deterministic hash values. +# @SIDE_EFFECT: Computes hash of artifact set +# @DATA_CONTRACT: ArtifactSet -> Manifest from __future__ import annotations diff --git a/backend/src/services/clean_release/preparation_service.py b/backend/src/services/clean_release/preparation_service.py index d5b447c5..08e1b8d9 100644 --- a/backend/src/services/clean_release/preparation_service.py +++ b/backend/src/services/clean_release/preparation_service.py @@ -1,10 +1,12 @@ -# #region PreparationService [C:3] [TYPE Module] [SEMANTICS clean-release, prepare, validate, policy, manifest] +# #region PreparationService [C:5] [TYPE Module] [SEMANTICS clean-release, prepare, validate, policy, manifest] # @BRIEF Prepare release candidate by policy evaluation and deterministic manifest creation. # @LAYER: Domain # @RELATION DEPENDS_ON -> [PolicyEngine] # @RELATION DEPENDS_ON -> [ManifestBuilder] # @RELATION DEPENDS_ON -> [RepositoryRelations] # @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically. +# @SIDE_EFFECT: Persists candidate and manifest +# @DATA_CONTRACT: PrepareRequest -> PrepareResult from __future__ import annotations diff --git a/backend/src/services/clean_release/publication_service.py b/backend/src/services/clean_release/publication_service.py index 5c35490e..3cac604c 100644 --- a/backend/src/services/clean_release/publication_service.py +++ b/backend/src/services/clean_release/publication_service.py @@ -209,4 +209,4 @@ def revoke_publication( # #endregion revoke_publication -# #endregion backend.src.services.clean_release.publication_service +# #endregion PublicationService diff --git a/backend/src/services/clean_release/repository.py b/backend/src/services/clean_release/repository.py index 960063a4..4b1f0b5f 100644 --- a/backend/src/services/clean_release/repository.py +++ b/backend/src/services/clean_release/repository.py @@ -1,8 +1,12 @@ -# #region RepositoryRelations [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, repository, relations, release] +# #region RepositoryRelations [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, repository, relations, release] # @BRIEF Provide repository adapter for clean release entities with deterministic access methods. -# @LAYER: Infra +# @LAYER: Infrastructure # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @INVARIANT: Repository operations are side-effect free outside explicit save/update calls. +# @PRE: In-memory storage initialized +# @POST: Repository operations exported +# @SIDE_EFFECT: Modifies in-memory state on save/update +# @DATA_CONTRACT: Entity -> RepositoryOperation from __future__ import annotations diff --git a/backend/src/services/clean_release/source_isolation.py b/backend/src/services/clean_release/source_isolation.py index 3708b6f6..e1946a31 100644 --- a/backend/src/services/clean_release/source_isolation.py +++ b/backend/src/services/clean_release/source_isolation.py @@ -1,8 +1,12 @@ -# #region SourceIsolation [C:3] [TYPE Module] [SEMANTICS clean-release, source, isolation, validate, resource] +# #region SourceIsolation [C:5] [TYPE Module] [SEMANTICS clean-release, source, isolation, validate, resource] # @BRIEF Validate that all resource endpoints belong to the approved internal source registry. # @LAYER: Domain # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @INVARIANT: Any endpoint outside enabled registry entries is treated as external-source violation. +# @PRE: Source registry configured +# @POST: Source isolation violations identified +# @SIDE_EFFECT: None (read-only check) +# @DATA_CONTRACT: SourceURL -> ViolationReport from __future__ import annotations diff --git a/backend/src/services/clean_release/stages/__init__.py b/backend/src/services/clean_release/stages/__init__.py index 245334a7..baa1b742 100644 --- a/backend/src/services/clean_release/stages/__init__.py +++ b/backend/src/services/clean_release/stages/__init__.py @@ -1,9 +1,11 @@ -# #region ComplianceStages [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, stage, package] +# #region ComplianceStages [C:5] [TYPE Module] [SEMANTICS clean-release, compliance, stage, package] # @BRIEF Define compliance stage order and helper functions for deterministic run-state evaluation. # @LAYER: Domain # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @RELATION DEPENDS_ON -> [ComplianceStageBase] # @INVARIANT: Stage order remains deterministic for all compliance runs. +# @SIDE_EFFECT: Registers compliance stages +# @DATA_CONTRACT: StagePipeline -> ComplianceResult from __future__ import annotations diff --git a/backend/src/services/clean_release/stages/base.py b/backend/src/services/clean_release/stages/base.py index b7e189d5..a8d04b89 100644 --- a/backend/src/services/clean_release/stages/base.py +++ b/backend/src/services/clean_release/stages/base.py @@ -1,9 +1,11 @@ -# #region ComplianceStageBase [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, stage, compliance, context] +# #region ComplianceStageBase [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, stage, compliance, context] # @BRIEF Define shared contracts and helpers for pluggable clean-release compliance stages. # @LAYER: Domain # @RELATION DEPENDS_ON -> [CleanReleaseModels] # @RELATION DEPENDS_ON -> [LoggerModule] # @INVARIANT: Stage execution is deterministic for equal input context. +# @SIDE_EFFECT: None (deterministic execution) +# @DATA_CONTRACT: Context -> StageResult from __future__ import annotations diff --git a/backend/src/services/clean_release/stages/data_purity.py b/backend/src/services/clean_release/stages/data_purity.py index 6f07e617..9a97428a 100644 --- a/backend/src/services/clean_release/stages/data_purity.py +++ b/backend/src/services/clean_release/stages/data_purity.py @@ -1,9 +1,11 @@ -# #region data_purity [C:3] [TYPE Module] [SEMANTICS clean-release, data, purity, validate, manifest] +# #region data_purity [C:5] [TYPE Module] [SEMANTICS clean-release, data, purity, validate, manifest] # @BRIEF Evaluate manifest purity counters and emit blocking violations for prohibited artifacts. # @LAYER: Domain # @RELATION IMPLEMENTS -> [ComplianceStage] # @RELATION DEPENDS_ON -> [ComplianceStageBase] # @INVARIANT: prohibited_detected_count > 0 always yields BLOCKED stage decision. +# @SIDE_EFFECT: None (read-only validation) +# @DATA_CONTRACT: Manifest -> DataPurityVerdict from __future__ import annotations diff --git a/backend/src/services/clean_release/stages/internal_sources_only.py b/backend/src/services/clean_release/stages/internal_sources_only.py index 1f044066..58d0eed9 100644 --- a/backend/src/services/clean_release/stages/internal_sources_only.py +++ b/backend/src/services/clean_release/stages/internal_sources_only.py @@ -1,9 +1,11 @@ -# #region internal_sources_only [C:3] [TYPE Module] [SEMANTICS clean-release, internal, source, validate] +# #region internal_sources_only [C:5] [TYPE Module] [SEMANTICS clean-release, internal, source, validate] # @BRIEF Verify manifest-declared sources belong to trusted internal registry allowlist. # @LAYER: Domain # @RELATION IMPLEMENTS -> [ComplianceStage] # @RELATION DEPENDS_ON -> [ComplianceStageBase] # @INVARIANT: Any source host outside allowed_hosts yields BLOCKED decision with at least one violation. +# @SIDE_EFFECT: None (read-only validation) +# @DATA_CONTRACT: Sources -> SourceViolationReport from __future__ import annotations diff --git a/backend/src/services/clean_release/stages/manifest_consistency.py b/backend/src/services/clean_release/stages/manifest_consistency.py index c3ca545f..c61ed176 100644 --- a/backend/src/services/clean_release/stages/manifest_consistency.py +++ b/backend/src/services/clean_release/stages/manifest_consistency.py @@ -1,9 +1,11 @@ -# #region manifest_consistency [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, consistency, validate] +# #region manifest_consistency [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, consistency, validate] # @BRIEF Ensure run is bound to the exact manifest snapshot and digest used at run creation time. # @LAYER: Domain # @RELATION IMPLEMENTS -> [ComplianceStage] # @RELATION DEPENDS_ON -> [ComplianceStageBase] # @INVARIANT: Digest mismatch between run and manifest yields ERROR with blocking violation evidence. +# @SIDE_EFFECT: None (read-only validation) +# @DATA_CONTRACT: RunData -> ConsistencyVerdict from __future__ import annotations diff --git a/backend/src/services/clean_release/stages/no_external_endpoints.py b/backend/src/services/clean_release/stages/no_external_endpoints.py index a2d9a43a..ec93897d 100644 --- a/backend/src/services/clean_release/stages/no_external_endpoints.py +++ b/backend/src/services/clean_release/stages/no_external_endpoints.py @@ -1,9 +1,11 @@ -# #region no_external_endpoints [C:3] [TYPE Module] [SEMANTICS clean-release, endpoint, validate, manifest, compliance] +# #region no_external_endpoints [C:5] [TYPE Module] [SEMANTICS clean-release, endpoint, validate, manifest, compliance] # @BRIEF Block manifest payloads that expose external endpoints outside trusted schemes and hosts. # @LAYER: Domain # @RELATION IMPLEMENTS -> [ComplianceStage] # @RELATION DEPENDS_ON -> [ComplianceStageBase] # @INVARIANT: Endpoint outside allowed scheme/host always yields BLOCKED stage decision. +# @SIDE_EFFECT: None (read-only validation) +# @DATA_CONTRACT: Endpoints -> EndpointViolationReport from __future__ import annotations diff --git a/backend/src/services/git/_remote_providers.py b/backend/src/services/git/_remote_providers.py index 687bf5cf..1e99fec1 100644 --- a/backend/src/services/git/_remote_providers.py +++ b/backend/src/services/git/_remote_providers.py @@ -50,47 +50,8 @@ class GitServiceGithubMixin: pass raise HTTPException(status_code=response.status_code, detail=f"GitHub API error: {detail}") return response.json() - # endregion create_github_repository - - # region create_github_pull_request [TYPE Function] - # @PURPOSE: Create pull request in GitHub or GitHub Enterprise. - # @PRE: Config and remote URL are valid. - # @POST: Returns normalized PR metadata. - # @RETURN: Dict[str, Any] - async def create_github_pull_request( - self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str, - title: str, description: str | None = None, draft: bool = False, - ) -> dict[str, Any]: - identity = self._parse_remote_repo_identity(remote_url) - base_url = self._normalize_git_server_url(server_url) - if "github.com" in base_url: - api_url = f"https://api.github.com/repos/{identity['namespace']}/{identity['repo']}/pulls" - else: - api_url = f"{base_url}/api/v3/repos/{identity['namespace']}/{identity['repo']}/pulls" - headers = { - "Authorization": f"token {pat.strip()}", - "Content-Type": "application/json", - "Accept": "application/vnd.github+json", - } - payload = { - "title": title, "head": from_branch, "base": to_branch, - "body": description or "", "draft": bool(draft), - } - try: - response = await self._http_client.post(api_url, headers=headers, json=payload) - except Exception as e: - raise HTTPException(status_code=503, detail=f"GitHub API is unavailable: {e!s}") - if response.status_code >= 400: - detail = response.text - try: - detail = response.json().get("message") or detail - except Exception: - pass - raise HTTPException(status_code=response.status_code, detail=f"GitHub API error: {detail}") - data = response.json() - return {"id": data.get("number") or data.get("id"), "url": data.get("html_url") or data.get("url"), "status": data.get("state") or "open"} # endregion create_github_pull_request - +# #endregion GitServiceGithubMixin # #region GitServiceGitlabMixin [C:3] [TYPE Class] # @BRIEF Mixin providing GitLab API operations for GitService. @@ -176,4 +137,3 @@ class GitServiceGitlabMixin: # endregion create_gitlab_merge_request # #endregion GitServiceGitlabMixin # #endregion GitServiceRemoteMixin -# #endregion GitServiceRemoteMixin diff --git a/backend/src/services/mapping_service.py b/backend/src/services/mapping_service.py index 794ef674..32ba4956 100644 --- a/backend/src/services/mapping_service.py +++ b/backend/src/services/mapping_service.py @@ -1,7 +1,7 @@ -# #region mapping_service [C:3] [TYPE Module] [SEMANTICS mapping, database, superset, fuzzy, suggestion] +# #region mapping_service [C:5] [TYPE Module] [SEMANTICS mapping, database, superset, fuzzy, suggestion] # # @BRIEF Orchestrates database fetching and fuzzy matching suggestions. -# @LAYER: Service +# @LAYER: Service # @PRE: source/target environment identifiers are provided by caller. # @POST: Exposes stateless mapping suggestion orchestration over configured environments. # @SIDE_EFFECT: Performs remote metadata reads through Superset API clients. diff --git a/backend/src/services/reports/__tests__/test_report_normalizer.py b/backend/src/services/reports/__tests__/test_report_normalizer.py index 3a121138..49a9f0e3 100644 --- a/backend/src/services/reports/__tests__/test_report_normalizer.py +++ b/backend/src/services/reports/__tests__/test_report_normalizer.py @@ -2,7 +2,7 @@ # @SEMANTICS: tests, reports, normalizer, fallback # @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior. # @RELATION: TESTS ->[normalize_report:Function] -# @LAYER: Domain (Tests) +# @LAYER: Domain # @INVARIANT: Unknown plugin types are mapped to canonical unknown task type. from datetime import datetime diff --git a/backend/src/services/resource_service.py b/backend/src/services/resource_service.py index 7ede82cf..5566afff 100644 --- a/backend/src/services/resource_service.py +++ b/backend/src/services/resource_service.py @@ -1,11 +1,13 @@ -# #region ResourceServiceModule [C:3] [TYPE Module] [SEMANTICS resource, git, task, status, superset] +# #region ResourceServiceModule [C:5] [TYPE Module] [SEMANTICS resource, git, task, status, superset] # @BRIEF Shared service for fetching resource data with Git status and task status -# @LAYER: Service +# @LAYER: Service # @RELATION DEPENDS_ON -> [SupersetClient] # @RELATION DEPENDS_ON -> [TaskManagerPackage] # @RELATION DEPENDS_ON -> [TaskManagerModels] # @RELATION DEPENDS_ON -> [GitService] # @INVARIANT: All resources include metadata about their current state +# @SIDE_EFFECT: Queries multiple backends for status +# @DATA_CONTRACT: ResourceQuery -> ResourceStatusSummary from datetime import UTC, datetime from typing import Any diff --git a/frontend/src/lib/components/layout/Breadcrumbs.svelte b/frontend/src/lib/components/layout/Breadcrumbs.svelte index 954538e1..b5a1f007 100644 --- a/frontend/src/lib/components/layout/Breadcrumbs.svelte +++ b/frontend/src/lib/components/layout/Breadcrumbs.svelte @@ -76,8 +76,8 @@ function formatBreadcrumbLabel(segment) { const specialCases = { dashboards: "nav.dashboard", - datasets: "nav.tools_mapper", - storage: "nav.tools_storage", + datasets: "nav.datasets", + storage: "nav.storage", admin: "nav.admin", settings: "nav.settings", git: "nav.git", diff --git a/frontend/src/lib/components/layout/sidebarNavigation.js b/frontend/src/lib/components/layout/sidebarNavigation.js index 407802c8..59c21e5d 100644 --- a/frontend/src/lib/components/layout/sidebarNavigation.js +++ b/frontend/src/lib/components/layout/sidebarNavigation.js @@ -154,7 +154,6 @@ export function buildSidebarSections(i18nState, user, features = {}) { requiredPermission: "plugin:storage", requiredAction: "READ", subItems: [ - { label: nav.backups, path: "/storage/backups", requiredPermission: "plugin:storage", requiredAction: "READ" }, { label: nav.repositories, path: "/storage/repos", requiredPermission: "plugin:storage", requiredAction: "READ" }, ], }, diff --git a/specs/030-dataset-lifecycle-workspace/checklists/requirements.md b/specs/030-dataset-lifecycle-workspace/checklists/requirements.md new file mode 100644 index 00000000..52ebd10d --- /dev/null +++ b/specs/030-dataset-lifecycle-workspace/checklists/requirements.md @@ -0,0 +1,71 @@ +# Requirements Checklist: Dataset Lifecycle Workspace + +**Purpose**: Validate completeness of functional and non-functional requirements for the Dataset Lifecycle Workspace feature. +**Created**: 2026-05-19 +**Feature**: [spec.md](../spec.md) + +## Functional Coverage + +- [ ] CHK001 Stats Bar renders 4 metrics (Все, Без маппинга, С описанием, С дашбордами) in page header +- [ ] CHK002 Each Stats Bar metric is clickable and filters the dataset list accordingly +- [ ] CHK003 Active filter is visually highlighted; clicking again resets filter +- [ ] CHK004 Split-view layout renders with 40% left panel and 60% right panel on desktop +- [ ] CHK005 Right panel shows placeholder "Выберите датасет" when no dataset is selected +- [ ] CHK006 Mobile layout (< 768px) uses full-width list with slide-over for detail panel +- [ ] CHK007 Dataset cards display: table_name, schema, column_count, metric_count, progress bar, quick action buttons +- [ ] CHK008 Progress bar shows color-coded fill: green (100%), yellow (≥50%), blue (<50%) +- [ ] CHK009 Right panel header shows: table_name, schema, column_count, metric_count, linked_dashboard_count, created_on +- [ ] CHK010 Right panel has "Columns" section with table (column_name, verbose_name, type, description, ✏️ inline edit) +- [ ] CHK011 Right panel has "Metrics" section with table (metric_name, expression, verbose_name, description, ✏️ inline edit) +- [ ] CHK012 verbose_name displayed in bold; fallback to column_name/metric_name in italic when absent +- [ ] CHK013 Missing description shows "✏️ Добавить описание" / "✏️ Add description" action prompt +- [ ] CHK014 Metric without description shows expression as hint below the name +- [ ] CHK015 Inline edit opens textarea with "Save"/"Cancel" buttons +- [ ] CHK016 Existing description is pre-filled in inline edit textarea +- [ ] CHK017 Save sends PUT /api/datasets/{id} with updated description +- [ ] CHK018 Save success shows toast and closes textarea +- [ ] CHK019 Save failure shows error message and keeps textarea open +- [ ] CHK020 Cards have checkboxes; selecting 2+ shows floating bulk action bar +- [ ] CHK021 Bulk Map Columns modal reused from existing implementation +- [ ] CHK022 Bulk Generate Docs modal reused from existing implementation +- [ ] CHK023 Deselecting all hides the bulk action bar + +## Backend API Coverage + +- [ ] CHK024 DatasetDetailResponse includes `metrics: list[MetricItem]` field +- [ ] CHK025 DatasetDetailResponse includes `metric_count: int` field +- [ ] CHK026 MetricItem Pydantic DTO has fields: id, metric_name, expression, verbose_name, description, metric_type +- [ ] CHK027 SupersetClient.get_dataset_detail extracts metrics from Superset API response +- [ ] CHK028 API correctly handles datasets with zero metrics (empty list, count 0) + +## i18n Coverage + +- [ ] CHK029 New EN keys: all, without_mapping, with_description, linked_to_dashboards, metrics, metric_count, no_selection, save_description, add_description +- [ ] CHK030 New RU keys: все, без_маппинга, с_описанием, связаны_с_дашбордами, метрики, метрик, выберите_датасет, сохранить_описание, добавить_описание +- [ ] CHK031 Breadcrumb for /datasets reads "Датасеты" (RU) / "Datasets" (EN) + +## Cross-Cutting Concerns + +- [ ] CHK032 Stats Bar values correctly update when switching environment +- [ ] CHK033 Existing pagination works with filtered dataset list +- [ ] CHK034 Search input filters work alongside Stats Bar active filter +- [ ] CHK035 Loading states for Stats Bar, card list, and detail panel +- [ ] CHK036 Error states with retry for list, detail, and inline save failures +- [ ] CHK037 No breaking changes to existing API endpoints (backward compatible) +- [ ] CHK038 No regression in existing bulk map/generate docs workflows +- [ ] CHK039 Responsive behavior at breakpoints: 1280px, 1024px, 768px, 480px + +## Out of Scope (Negative Checklist) + +- [ ] CHK040 Drag-and-drop колонок NOT implemented +- [ ] CHK041 CSV export NOT implemented +- [ ] CHK042 Side-by-side сравнение датасетов NOT implemented +- [ ] CHK043 Тёмная тема NOT implemented +- [ ] CHK044 Scheduled автоматический маппинг NOT implemented + +## Notes + +- Check items off as completed: `[x]` +- Add comments or findings inline +- Link to relevant tasks from tasks.md after decomposition +- Items CHK040-CHK044 serve as scope boundaries to prevent creep diff --git a/specs/030-dataset-lifecycle-workspace/spec.md b/specs/030-dataset-lifecycle-workspace/spec.md new file mode 100644 index 00000000..cb71faa9 --- /dev/null +++ b/specs/030-dataset-lifecycle-workspace/spec.md @@ -0,0 +1,200 @@ +# Feature Specification: Dataset Lifecycle Workspace + +**Feature Branch**: `030-dataset-lifecycle-workspace` +**Created**: 2026-05-19 +**Status**: Draft +**Input**: Feature 029 — Dataset Lifecycle Workspace (variant C) + +## Clarifications + +### Session 2026-05-19 + +- Q: Stats Bar фильтрует клиентски или серверно? → A: Клиентская фильтрация по данным из API. Бэкенд в ответе `GET /api/datasets` возвращает 4 stats-числа (`total`, `unmapped_count`, `mapped_count`, `linked_count`). Клиенты фильтруют карточки без дополнительного запроса. Пагинация остаётся серверной. При фильтре пагинация переключает страницы отфильтрованного подмножества. +- Q: Каким запросом сохранять inline-edit описания колонок/метрик? → A: Два новых ss-tools эндпоинта: `PUT /api/datasets/{id}/columns/{col_id}/description` и `PUT /api/datasets/{id}/metrics/{metric_id}/description` (body: `{description: "text"}`). Бэкенд выполняет GET датасета из Superset, модифицирует одну колонку/метрику, PUT обратно полный объект. В Superset нет PATCH — только PUT с полным датасетом. +- Q: Как обрабатывать конфликт одновременного редактирования? → A: Last-write-wins. Бэкенд всегда выполняет GET→modify→PUT без проверки версии. Последний сохранивший перезаписывает. Для описаний колонок/метрик это приемлемый риск. ETag/блокировки — out of scope. +- Q: Параметр `override_columns` при PUT в Superset? → A: Всегда `override_columns=false`. Бэкенд получает полный датасет из GET, модифицирует только description нужной колонки, PUT обратно с `override_columns=false`. Superset сохраняет все колонки, не участвовавшие в payload. +- Q: Как Stats Bar узнаёт об изменении counts после асинхронных задач? → A: WebSocket push. При завершении mapping/docs задачи сервер отправляет событие `dataset.updated` через существующий WebSocket. Фронтенд по событию перезапрашивает `GET /api/datasets` и обновляет Stats Bar + карточки автоматически. + +## User Scenarios & Testing + +### User Story 1 — Stats-Driven Overview & Filtering (Priority: P1) + +Пользователь заходит на страницу датасетов и видит 4 ключевые метрики в Stats Bar вверху. Каждая метрика кликабельна как фильтр списка. Метрики дают мгновенный обзор состояния датасетов и позволяют быстро отфильтровать те, которые требуют внимания. + +**Why this priority**: Stats bar — первое, что видит пользователь. Он заменяет текущую пустую область над таблицей и даёт моментальный actionable insight. Без него страница неотличима от текущей реализации. + +**Independent Test**: Перейти на `/datasets`, проверить что каждая из 4 метрик отображается и клик по ней фильтрует список. + +**Acceptance Scenarios**: + +1. **Given** пользователь видит страницу датасетов с загруженными данными, **When** страница загружена, **Then** Stats Bar показывает 4 метрики: Все (total), Без маппинга, С описанием, С дашбордами +2. **Given** Stats Bar отображается, **When** пользователь кликает на "Без маппинга", **Then** список фильтруется до датасетов с `mapped_fields.mapped === 0`, метрика подсвечена +3. **Given** Stats Bar отображается, **When** пользователь кликает на уже активный фильтр, **Then** фильтр сбрасывается и показываются все датасеты +4. **Given** API возвращает ошибку при загрузке, **When** страница рендерится, **Then** Stats Bar показывает 0 для всех метрик или состояние ошибки + +--- + +### User Story 2 — Split-View с карточками и детальной панелью (Priority: P1) + +Пользователь видит split-view: слева (40%) — список датасетов в виде карточек с прогресс-баром маппинга, справа (60%) — детальный просмотр выбранного датасета без перезагрузки страницы. На мобильных (< 768px) список занимает весь экран, а детали открываются как slide-over. + +**Why this priority**: Split-view — ядро workspace. Без него нет смысла в Stats Bar и inline-редактировании. Это главное UX-изменение относительно текущей таблицы + отдельной страницы деталей. + +**Independent Test**: Выбрать датасет в левой панели, убедиться что правая панель показывает его детали без перезагрузки страницы. + +**Acceptance Scenarios**: + +1. **Given** страница загружена, **When** список датасетов отрендерен, **Then** карточки отображаются в левой панели (40%), правая панель показывает placeholder "Выберите датасет" +2. **Given** список загружен, **When** пользователь кликает на карточку датасета, **Then** правая панель загружает детали выбранного датасета (имя, схема, колонки, метрики, дашборды) +3. **Given** датасет выбран в правой панели, **When** пользователь кликает на другую карточку, **Then** правая панель обновляется без полной перезагрузки страницы +4. **Given** ширина экрана < 768px, **When** пользователь кликает на карточку, **Then** правая панель открывается как slide-over поверх списка +5. **Given** slide-over открыт на мобильном, **When** пользователь кликает "Назад" или вне slide-over, **Then** slide-over закрывается, возвращаясь к списку + +--- + +### User Story 3 — Карточки датасетов с прогресс-баром (Priority: P2) + +Каждая карточка в левой панели содержит: имя таблицы, схему, количество колонок, количество метрик, прогресс-бар маппинга (mapped/total в процентах), кнопки быстрых действий (Маппинг, Доки). Прогресс-бар визуально показывает состояние маппинга. + +**Why this priority**: Карточки — основной навигационный элемент. Без них split-view бесполезен, но технически карточки зависят от структуры split-view (US2). + +**Independent Test**: Проверить что карточка содержит все обязательные поля и прогресс-бар правильно отражает `mapped/total`. + +**Acceptance Scenarios**: + +1. **Given** список датасетов загружен, **When** карточки отрендерены, **Then** каждая карточка показывает: имя таблицы, схему, кол-во колонок, кол-во метрик, прогресс-бар +2. **Given** датасет имеет `mapped_fields: {total: 10, mapped: 7}`, **When** карточка рендерится, **Then** прогресс-бар заполнен на 70% зелёным/жёлтым/синим в зависимости от процента +3. **Given** датасет без метрик (metric_count = 0), **When** карточка рендерится, **Then** кол-во метрик показывает "0" или скрыто +4. **Given** карточка отображается, **When** пользователь нажимает кнопку "Маппинг", **Then** открывается модалка маппинга колонок для этого датасета + +--- + +### User Story 4 — Детальная панель с колонками и метриками (Priority: P2) + +Правая панель показывает: хедер датасета (полное имя, схема, кол-во колонок/метрик, связанные дашборды, дата создания), секцию "Колонки" (таблица с column_name, verbose_name, type, description, кнопка inline-редактирования), секцию "Метрики" (таблица с metric_name, expression, verbose_name, description, кнопка inline-редактирования), связанные дашборды. + +**Why this priority**: Детальная панель — вторая половина split-view. Без неё нет рабочего пространства, но технически зависит от US2. + +**Independent Test**: Выбрать датасет с метриками и колонками, проверить что обе секции отображаются корректно. + +**Acceptance Scenarios**: + +1. **Given** датасет выбран, **When** правая панель загружена, **Then** хедер показывает: `table_name`, `schema`, `column_count`, `metric_count`, `linked_dashboard_count`, `created_on` +2. **Given** у колонки есть `verbose_name`, **When** секция колонок рендерится, **Then** `verbose_name` показывается жирным, `column_name` не показывается +3. **Given** у колонки нет `verbose_name`, **When** секция колонок рендерится, **Then** `column_name` показывается курсивом как fallback +4. **Given** у колонки/метрики нет `description`, **When** строка рендерится, **Then** показывается "✏️ Добавить описание" как action prompt +5. **Given** у метрики нет `description`, **When** строка метрики рендерится, **Then** под именем показывается `expression` как подсказка + +--- + +### User Story 5 — Inline-редактирование описаний (Priority: P3) + +Пользователь может редактировать description колонок и метрик прямо в детальной панели. Клик по ✏️ на строке открывает textarea, пользователь вводит текст, сохраняет через API. Изменения применяются без перезагрузки страницы. + +**Why this priority**: Inline-редактирование — ключевая фича workspace, но она менее критична чем базовая навигация и просмотр. Зависит от US4 (детальная панель) и бэкенд-эндпоинта. + +**Independent Test**: Кликнуть ✏️ на колонке, ввести описание, сохранить — проверить что описание обновилось и отображается. + +**Acceptance Scenarios**: + +1. **Given** строка колонки без описания, **When** пользователь кликает "✏️ Добавить описание", **Then** появляется textarea с кнопками "Сохранить" и "Отмена" +2. **Given** textarea открыта, **When** пользователь вводит текст и нажимает "Сохранить", **Then** описание сохраняется через PUT `/api/datasets/{id}`, textarea закрывается, описание отображается +3. **Given** textarea открыта, **When** пользователь нажимает "Отмена", **Then** textarea закрывается без изменений +4. **Given** сохранение не удалось (ошибка API), **When** пользователь нажимает "Сохранить", **Then** показывается сообщение об ошибке, textarea остаётся открытой для повторной попытки +5. **Given** строка колонки с существующим описанием, **When** пользователь кликает ✏️, **Then** textarea открывается с предзаполненным текстом описания + +--- + +### User Story 6 — Bulk-режим с нижней панелью (Priority: P3) + +Чекбоксы на карточках позволяют выбрать несколько датасетов. При выборе 2+ датасетов появляется нижняя панель с bulk-действиями: "Маппинг колонок" и "Генерация документации". Модалки bulk-маппинга и генерации доков переиспользуются из текущей реализации. + +**Why this priority**: Bulk-режим уже существует в текущей реализации, его нужно адаптировать под карточки в split-view. Менее приоритетно чем базовый просмотр. + +**Independent Test**: Выбрать 3 датасета через чекбоксы, проверить что нижняя панель появляется и кнопки работают. + +**Acceptance Scenarios**: + +1. **Given** карточки отображаются с чекбоксами, **When** пользователь отмечает 2+ датасета, **Then** нижняя панель появляется с кнопками "Маппинг колонок" и "Генерация документации" +2. **Given** нижняя панель видна, **When** пользователь снимает выделение со всех датасетов, **Then** панель исчезает +3. **Given** нижняя панель видна, **When** пользователь нажимает "Маппинг колонок", **Then** открывается модалка маппинга (переиспользованная из текущей реализации) +4. **Given** выбраны датасеты с разных страниц пагинации, **When** нижняя панель отображается, **Then** счётчик показывает общее количество выбранных датасетов + +--- + +### User Story 7 — Отображение метрик в API и UI (Priority: P2) + +Бэкенд API возвращает метрики в `DatasetDetailResponse`. Фронтенд отображает их в секции "Метрики" детальной панели. + +**Why this priority**: Метрики — обязательная часть детальной панели (US4), но требуют изменений на бэкенде. Без бэкенд-поддержки UI нечего показывать. + +**Independent Test**: Вызвать `GET /api/datasets/{id}`, проверить наличие полей `metrics` и `metric_count` в ответе, и что `MetricItem` содержит все обязательные поля. + +**Acceptance Scenarios**: + +1. **Given** датасет имеет метрики в Superset API, **When** бэкенд получает `GET /api/datasets/{id}`, **Then** ответ содержит `metrics: [...]` и `metric_count` +2. **Given** у метрики есть все поля Superset API, **When** бэкенд трансформирует ответ, **Then** `MetricItem` содержит: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type` +3. **Given** Superset API возвращает датасет без метрик, **When** бэкенд обрабатывает ответ, **Then** `metrics: []` и `metric_count: 0` + +--- + +### Edge Cases + +- Что происходит когда API датасетов возвращает 503 (Superset недоступен)? +- Как обрабатывается случай когда `detail` API для выбранного датасета падает, но список загружен? +- Как ведёт себя split-view при ресайзе окна между десктопным и мобильным layout? +- Что показывать в Stats Bar когда данные ещё не загружены (loading state)? +- Как обрабатывать случай когда у датасета 0 колонок и 0 метрик? +- Что происходит при сохранении inline-edit когда несколько пользователей редактируют один датасет (конфликт)? → **Решено**: last-write-wins. Последний сохранивший перезаписывает. ETag/блокировки — out of scope. +- Как Stats Bar обновляет числа при переключении окружения — запрашивает новые counts? + +## Requirements + +### Functional Requirements + +- **FR-001**: Система MUST отображать Stats Bar с 4 метриками (Все, Без маппинга, С описанием, С дашбордами) вверху страницы `/datasets` +- **FR-002**: Каждая метрика в Stats Bar MUST быть кликабельной и действовать как фильтр списка датасетов +- **FR-003**: Система MUST отображать split-view layout: левая панель (40%) со списком карточек, правая панель (60%) с деталями выбранного датасета +- **FR-004**: При ширине экрана < 768px правая панель MUST открываться как slide-over поверх списка +- **FR-005**: Карточка датасета MUST содержать: имя таблицы, схему, количество колонок, количество метрик, прогресс-бар маппинга, кнопки быстрых действий +- **FR-006**: Прогресс-бар MUST визуально отображать процент маппинга (mapped/total) с цветовой кодировкой: зелёный (100%), жёлтый (≥50%), синий (<50%) +- **FR-007**: Правая панель MUST показывать хедер датасета (полное имя, схема, кол-во колонок/метрик, связанные дашборды, дата создания) +- **FR-008**: Правая панель MUST содержать секцию "Колонки" с таблицей (column_name, verbose_name, type, description, ✏️ inline edit) +- **FR-009**: Правая панель MUST содержать секцию "Метрики" с таблицей (metric_name, expression, verbose_name, description, ✏️ inline edit) +- **FR-010**: При наличии `verbose_name` система MUST показывать его жирным; при отсутствии — показывать `column_name`/`metric_name` курсивом +- **FR-011**: При отсутствии `description` система MUST показывать "✏️ Добавить описание" как action prompt +- **FR-012**: Для метрик без описания система MUST показывать `expression` как подсказку под именем +- **FR-013**: Inline-редактирование MUST поддерживать открытие textarea с кнопками "Сохранить"/"Отмена", с предзаполнением существующего описания +- **FR-014**: Сохранение описания колонки MUST выполняться через `PUT /api/datasets/{id}/columns/{col_id}/description` (body: `{description: "text"}`). Сохранение описания метрики — `PUT /api/datasets/{id}/metrics/{metric_id}/description`. Бэкенд выполняет GET→modify→PUT в Superset. +- **FR-015**: Карточки MUST содержать чекбоксы для bulk-выбора, при 2+ выбранных MUST появляться нижняя панель с bulk-действиями +- **FR-016**: Bulk-действия "Маппинг колонок" и "Генерация документации" MUST переиспользовать существующие модалки +- **FR-017**: `DatasetDetailResponse` на бэкенде MUST включать `metrics: list[MetricItem]` и `metric_count: int` +- **FR-018**: `MetricItem` Pydantic DTO MUST содержать поля: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type` +- **FR-019**: `SupersetClient.get_dataset_detail` MUST извлекать `dataset.get("metrics", [])` из ответа Superset API +- **FR-020**: Система MUST поддерживать i18n ключи (EN/RU) для: all, without_mapping, with_description, linked_to_dashboards, metrics, metric_count, no_selection, save_description, add_description +- **FR-021**: Breadcrumb для `/datasets` MUST отображать "Датасеты / Datasets" (EN/RU), а не "Маппер колонок" +- **FR-022**: Ответ `GET /api/datasets` MUST включать поля stats: `total` (int), `unmapped_count` (int), `mapped_count` (int), `linked_count` (int) — для отрисовки Stats Bar без дополнительных запросов +- **FR-023**: PUT в Superset при сохранении inline-edit MUST выполняться с `override_columns=false` (query parameter) — предотвращает случайное удаление колонок, не упомянутых в payload +- **FR-024**: По WebSocket-событию `dataset.updated` (отправляемому при завершении mapping/docs задачи) фронтенд MUST перезапрашивать `GET /api/datasets` и обновлять Stats Bar counts + карточки без ручного «Обновить» + +### Key Entities + +- **StatsBarMetric**: Метрика в Stats Bar — label, count, icon, filter_key, is_active +- **DatasetCard**: Карточка в левой панели — id, table_name, schema, column_count, metric_count, mapped_progress (%), quick_actions[] +- **DatasetDetailPanel**: Детальная панель — header_info, columns_table, metrics_table, linked_dashboards +- **MetricItem** (новый): id, metric_name, expression, verbose_name, description, metric_type +- **ColumnRow**: Строка в таблице колонок — column_name, verbose_name, type, description, is_editing (UI state) +- **MetricRow**: Строка в таблице метрик — metric_name, expression, verbose_name, description, is_editing (UI state) +- **BulkActionPanel**: Нижняя панель — selected_count, available_actions[], is_visible + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: Пользователь может за 1 клик перейти от Stats Bar к отфильтрованному списку датасетов +- **SC-002**: Переключение между датасетами в split-view происходит < 500ms (без полной перезагрузки страницы) +- **SC-003**: Пользователь может отредактировать и сохранить описание колонки/метрики < 3 кликов +- **SC-004**: Stats Bar корректно обновляет значения при переключении окружения +- **SC-005**: Все существующие bulk-действия (маппинг, генерация доков) продолжают работать в новом UI +- **SC-006**: Страница корректно адаптируется между десктопным split-view и мобильным slide-over без потери функциональности +- **SC-007**: Все новые строки интерфейса имеют переводы на EN и RU diff --git a/specs/030-dataset-lifecycle-workspace/ux_reference.md b/specs/030-dataset-lifecycle-workspace/ux_reference.md new file mode 100644 index 00000000..86b3290a --- /dev/null +++ b/specs/030-dataset-lifecycle-workspace/ux_reference.md @@ -0,0 +1,104 @@ +# UX Reference: Dataset Lifecycle Workspace + +**Feature Branch**: `030-dataset-lifecycle-workspace` +**Created**: 2026-05-19 +**Status**: Draft + +## 1. User Persona & Context + +- **Who is the user?**: Data Analyst / Data Engineer, работающий с датасетами Apache Superset через ss-tools. Технически подкован, понимает структуру таблиц, колонок и метрик. +- **What is their goal?**: Быстро оценить состояние датасетов (какие без маппинга, какие без описаний), просмотреть детали конкретного датасета, отредактировать описания колонок/метрик, запустить bulk-операции (маппинг, генерация документации). +- **Context**: Работает в браузере на десктопе (1920×1080) или ноутбуке (1366×768). Может использовать планшет. Всегда в рамках выбранного окружения (dev/staging/prod). + +## 2. The "Happy Path" Narrative + +Аналитик открывает страницу датасетов и сразу видит Stats Bar: 142 датасета всего, 38 без маппинга, 89 с описанием, 12 связаны с дашбордами. Он кликает "⚠️ Без маппинга" — список слева мгновенно фильтруется до 38 датасетов. Выбирает первый датасет в списке — справа без перезагрузки открывается детальная панель: хедер с именем таблицы и схемой, секция колонок с progress-индикаторами описаний, секция метрик. Видит колонку без описания, кликает "✏️ Добавить описание" — появляется textarea, вводит описание, сохраняет. Описание мгновенно отображается. Затем выделяет 3 датасета через чекбоксы, внизу появляется панель — запускает "Маппинг колонок" для всех трёх одним кликом. + +## 3. Interface Mockups + +### UI Layout & Flow + +**Page**: `/datasets` + +- **Layout**: Двухколоночный split-view (40/60) на десктопе. При < 768px — одноколоночный с slide-over для деталей. +- **Key Elements**: + - **Stats Bar**: 4 метрики в строке вверху, каждая — кнопка-фильтр с иконкой, числом и подписью. Активная метрика подсвечена (primary background). + - **Left Panel**: Список карточек датасетов с поиском и пагинацией. Каждая карточка содержит чекбокс. + - **Right Panel**: Детальная панель с хедером, таблицами колонок/метрик, связанными дашбордами. + - **Bulk Action Bar**: Fixed bottom panel, slides up when 2+ datasets selected. + - **Map Columns Modal**: Reused from existing implementation. + - **Generate Docs Modal**: Reused from existing implementation. +- **Contract Mapping**: + - **`@UX_STATE`**: + - `Loading` — Stats Bar skeleton + Left panel skeleton (5 placeholder cards). + - `Loaded` — Stats Bar with counts + left panel cards + right panel placeholder ("Выберите датасет"). + - `Loaded-Detail` — Right panel filled with dataset details. + - `Filtered` — Stats Bar with active metric highlighted + filtered card list. + - `Selecting` — Checkboxes checked on cards + floating bulk action panel visible. + - `Editing` — Inline textarea open on a column/metric row. + - `Saving` — Textarea disabled with spinner, save button shows "Сохранение...". + - `Error-Detail` — Right panel error banner with retry. + - `Error-List` — Full page error banner with retry. + - **`@UX_FEEDBACK`**: + - Stats Bar metric click → instant list filter + metric highlight. + - Dataset card click → right panel loads detail (spinner then content). + - Save description → "✓ Сохранено" toast + textarea closes. + - Save error → red border on textarea + "⚠️ Ошибка сохранения" below. + - Slide-over (mobile) → smooth slide from right with backdrop. + - **`@UX_RECOVERY`**: + - API error in right panel → click "Retry" to reload dataset detail. + - API error in list → click "Retry" to reload full list. + - Save conflict → show warning "Данные были изменены другим пользователем. Обновить?" + - Network offline → show offline banner, auto-retry on reconnect. + - **`@UX_REACTIVITY`**: + - `$state` for: `selectedDatasetId`, `activeFilter`, `selectedIds`, `editingCell` + - `$derived` for: `filteredDatasets` (based on `activeFilter` + `searchQuery`) + - `$effect` for: loading dataset detail when `selectedDatasetId` changes + - `$props` for: reusable card component (`DatasetCard.svelte`), detail panel component (`DatasetDetailPanel.svelte`) +- **States**: + - **Idle/Default**: Stats Bar shows counts, left panel shows paginated cards, right panel shows "Выберите датасет для просмотра деталей" placeholder. + - **Loading**: Skeleton loaders for Stats Bar (4 count placeholders), Left Panel (5 card placeholders with shimmer), Right Panel placeholder. + - **Success**: All data loaded, interactions responsive (filter, select, edit). + - **Error/Degraded**: If list fails → full page error. If detail fails → error in right panel only, list still navigable. + +## 4. The "Error" Experience + +**Philosophy**: Не просто сообщить об ошибке, а дать пользователю путь к восстановлению. + +**Semantic Requirement**: Every documented failure path here should map to `@UX_RECOVERY` and, where relevant, `@UX_FEEDBACK` in the generated component contracts. + +### Scenario A: API 503 (Superset недоступен) + +- **User Action**: Открывает страницу датасетов. +- **System Response**: + - (UI) Full-page error banner: "⚠️ Не удалось загрузить датасеты. Сервер Superset временно недоступен." + кнопка "Повторить". + - Stats Bar скрыт или показывает "--" для всех метрик. +- **Recovery**: Кнопка "Retry" перезагружает список. Автоматический retry через 30s. + +### Scenario B: Ошибка загрузки деталей датасета + +- **User Action**: Кликает на карточку датасета в левой панели. +- **System Response**: + - (UI) В правой панели появляется ошибка: "⚠️ Не удалось загрузить детали датасета." + кнопка "Повторить". + - Левая панель остаётся рабочей — пользователь может выбрать другой датасет. +- **Recovery**: Клик "Retry" перезагружает детали. Список остаётся навигабельным. + +### Scenario C: Ошибка сохранения описания (inline edit) + +- **User Action**: Вводит описание в textarea, нажимает "Сохранить". +- **System Response**: + - (UI) Красная обводка textarea + сообщение под ней: "⚠️ Ошибка сохранения. Проверьте соединение и попробуйте снова." + - Textarea остаётся открытой с введённым текстом (не теряется). +- **Recovery**: Повторное нажатие "Сохранить" либо "Отмена" для выхода без сохранения. + +### Scenario D: Конфликт при сохранении (409 Conflict) + +- **User Action**: Сохраняет описание, но данные были изменены другим пользователем. +- **System Response**: + - (UI) Предупреждение: "⚠️ Данные были изменены. Обновить страницу?" с кнопками "Обновить" и "Отмена". +- **Recovery**: Кнопка "Обновить" перезагружает детали датасета. + +## 5. Tone & Voice + +- **Style**: Concise, technical, task-oriented. Русский и английский. +- **Terminology**: "Датасет" (не "таблица" или "источник данных"), "Маппинг" (не "сопоставление"), "Колонка" (не "столбец" или "поле"), "Метрика" (не "показатель"). diff --git a/ui-audit-report.md b/ui-audit-report.md new file mode 100644 index 00000000..ec4be435 --- /dev/null +++ b/ui-audit-report.md @@ -0,0 +1,306 @@ +# UI Audit Report — Superset Tools (http://localhost:8100) + +**Дата проверки:** 19.05.2026 +**Пользователь:** admin / admin123 +**Окружение:** dev (https://ss-dev.bebesh.ru) +**Всего разделов:** ~20 страниц + +--- + +## Сводка + +| Категория | Кол-во | +|---|---| +| 🟢 Работает корректно | 8 | +| 🟡 Требует доработки | 7 | +| 🔴 Не работает / пусто | 5 | +| 🔄 Дублирование функционала | 4 | + +--- + +## 1. ДЭШБОРДЫ ( /dashboards ) + +### 1.1 Обзор ( /dashboards ) +**Статус:** 🟢 Работает +- Таблица с 10 дашбордами, загруженными из Superset +- Колонки: Заголовок, GIT, Статус LLM пров., Действия, Дата изменения, Владельцы +- Фильтры: поиск, сортировка по колонкам, "Активны фильтры профиля" +- Действия для каждого дашборда: Мигрировать, Проверить, Создать бэкап + +**Проблемы:** +- У всех дашбордов статус "НЕТ РЕПО" — Git не настроен ни для одного дашборда +- Кнопки "Выбрать все" / "Выбрать видимые" — нет индикатора сколько выбрано +- Breadcrumb показывает "nav.dashboard" (непереведённый ключ) + +### 1.2 Центр здоровья ( /dashboards/health ) +**Статус:** 🟡 Требует доработки +- Страница существует, есть фильтр по окружению +- **Нет данных:** все счётчики 0, "Записи валидации не найдены" +- Вероятно, не запускалась валидация дашбордов (LLM check) +- Пустая таблица деталей валидации + +--- + +## 2. ДАТАСЕТЫ + +### 2.1 Все датасеты ( /datasets ) +**Статус:** 🔴 Пустая страница +- Показывает только "Датасеты" + "Окружение: dev" +- **Нет списка датасетов**, нет таблицы, нет контента +- Breadcrumb показывает "Маппер колонок" — несоответствие навигации + +### 2.2 Review Workspace ( /datasets/review ) +**Статус:** 🟢 Работает +- Интерфейс для review-сессий датасетов +- Два способа старта: "Ссылка Superset" и "Выбор датасета" +- Селектор окружения, поле URL Superset +- Кнопка "Старт по ссылке" disabled (нет URL) + +--- + +## 3. МИГРАЦИЯ + +### 3.1 Обзор ( /migration ) +**Статус:** 🟡 Требует доработки +- Список задач миграции: 2 задачи system-debug (SUCCESS) +- Source и Target окружения — **только dev доступен** (мигрировать некуда) +- Кнопки "Dry Run" и "Запустить миграцию" **disabled** +- Фильтр "Fix Cross-Filters" и "Заменить БД" — есть, но неактивны без выбора дашбордов + +### 3.2 Маппинги ( /migration/mappings ) +**Статус:** 🔴 Не работает +- Выбор source/target окружений (только dev) +- Кнопка "Получить БД и подсказки" **disabled** +- Нет данных, нет таблицы маппингов + +--- + +## 4. GIT ( /git ) + +### 4.1 Состояние репозитория +**Статус:** 🟢 Работает (но данных нет) +- Список дашбордов с полями "ЗАГОЛОВОК", "ПОСЛЕДНЕЕ ИЗМЕНЕНИЕ", "СТАТУС" +- Все дашборды: "Нет репозитория" +- Пагинация: 10 дашбордов, кнопки "Назад"/"Вперед" disabled +- **Git не интегрирован** — ни один дашборд не привязан к репозиторию + +--- + +## 5. ПЕРЕВОДЫ (TRANSLATE) + +### 5.1 Задания ( /translate ) +**Статус:** 🟢 Работает +- Фильтры: All (1), Черновик, Готово, Выполняется, Завершено, Ошибка +- Одна задача "test" со статусом "READY" +- Действия: Дублировать, Удалить + +### 5.2 Словари ( /translate/dictionaries ) +**Статус:** 🟢 Работает (пусто) +- "Словарей пока нет", кнопка "Создайте первый словарь" + +### 5.3 История ( /translate/history ) +**Статус:** 🟡 Требует доработки +- **Счётчики не соответствуют:** "Всего запусков: 4", "Успешных: 0", "Ошибок: 3", но в списке задач было "Готово (1)" +- 4 запуска задачи "test": 3 FAILED (100% ошибок), 1 CANCELLED (78 ошибок) +- Все строки в каждом запуске: "0 ок" / "100 ошибок" — **перевод полностью не работает** +- Кнопка "Повторить ошибки" есть, но неясно работает ли +- Фильтры: по задаче, статусу, триггеру, дате + +--- + +## 6. ОТЧЕТЫ ( /reports ) + +**Статус:** 🟡 Требует доработки +- 2 отчёта от system-debug задач (242377ce, 5f557d5e) +- **Оба отображаются как "Прочее / Неизвестный тип"** — классификация не работает +- Фильтры: по типу (LLM, Бэкапы, Миграция, Documentation) и статусу +- **UX-проблема:** Сырой JSON (дамп Superset API) отображается прямо на странице как текст — нечитаемо для пользователя + +--- + +## 7. ИНСТРУМЕНТЫ (TOOLS) + +### 7.1 Маппер колонок ( /tools/mapper ) +**Статус:** 🟢 Работает +- Форма: окружение, ID датасета, источник маппинга (PostgreSQL/Excel) +- "Сохраненное подключение" — пустой выпадающий список +- Кнопки: "Сгенерировать документацию", "Запустить маппер" +- **Дублирование:** тот же "Маппер колонок" доступен из раздела "Датасеты" + +### 7.2 Диагностика ( /tools/debug ) +**Статус:** 🟢 Работает +- 2 действия: "Проверить Database API" и "Получить структуру датасета (JSON)" +- Source/Target окружения (только dev) +- Кнопка "Запустить диагностику" + +### 7.3 Файловое хранилище ( /tools/storage ) +**Статус:** 🟡 Требует доработки +- 2 папки: `backups` (11.03.2026) и `repositorys` (17.05.2026) +- **Опечатка:** "repositorys" → должно быть "repositories" +- Загрузка файлов по категориям: Бэкапы / Репозитории +- **Дублирование:** "Хранилище > Бэкапы" ведёт на этот же URL (/tools/storage) + +### 7.4 Бэкапы ( /tools/backups ) +**Статус:** 🟡 Требует доработки +- 3 существующих бэкапа: SS-PREPROD, SS1, SS2 (без дат — "--") +- Кнопка "Начать бэкап" **disabled** (не выбран окружение) +- Кнопка "Перейти к хранилищу" — ведёт в /tools/storage + +--- + +## 8. ХРАНИЛИЩЕ (STORAGE) + +### 8.1 Бэкапы → /tools/storage +**Статус:** 🔄 Дублирует "Инструменты > Файловое хранилище" + +### 8.2 Репозитории ( /storage/repos ) +**Статус:** 🔴 Не работает +- Зависает на "Загрузка..." — данные не загружаются +- Breadcrumb: Файловое хранилище > Repos + +--- + +## 9. АДМИН (ADMIN) + +### 9.1 Пользователи ( /admin/users ) +**Статус:** 🟢 Работает +- 1 пользователь: admin (LOCAL, Admin, Активен) +- CRUD: Создать, Редактировать, Удалить + +### 9.2 Роли ( /admin/roles ) +**Статус:** 🟢 Работает +- 1 роль: Admin (System Administrator) +- CRUD: Создать, Редактировать, Удалить + +### 9.3 Настройка ADFS ( /admin/settings ) +**Статус:** 🟢 Работает +- AD group mapping (пусто) — кнопка "Добавить маппинг" +- Настройка логирования: уровень DEBUG (приложение), INFO (задачи) +- Belief State logging (Beta) — включён + +### 9.4 Провайдеры LLM ( /admin/settings/llm ) +**Статус:** 🟢 Работает +- 1 провайдер: "kilo free" (qwen/qwen3.6-flash), активен +- Назначение задач: Dashboard check, Documentation, Git commit +- Шаблоны промптов (английские, редактируемые) +- **Проблема:** все провайдеры по умолчанию стоят "По умолчанию" — неясно какой провайдер используется + +--- + +## 10. СИСТЕМНЫЕ ЭЛЕМЕНТЫ + +### 10.1 AI Ассистент (слайд-панель) +**Статус:** 🟡 Требует доработки +- Открывается как боковая панель +- Диалоги: Новый, Активные (1), Архив (0) +- **Команда "Привет" не распознана** — ответ "Команда неоднозначна. Уточните действие: git / migration / backup / llm / status" +- Нет приветственного сообщения или онбординга +- UI мелкий, трудночитаемый + +### 10.2 Активность (панель задач) +**Статус:** 🟢 Работает +- Показывает последние фоновые задачи +- Кнопка "Отчеты" (ведёт в /reports) + +### 10.3 Настройки ( /settings ) — шестерёнка +**Статус:** 🟡 Требует доработки +- **Дублирует** часть функционала из "Админ" +- Вкладки: Окружения, Логирование, Подключения, Git, LLM, Синхронизация миграции, Хранилище, Разделы проекта, Automation +- Окружения: 1 запись (dev, https://ss-dev.bebesh.ru) +- Много overlap с админ-панелью + +### 10.4 Языковой переключатель (Русский / English) +**Статус:** 🟡 Требует доработки +- Переключение между Русским и English +- **Смешанные языки:** breadcrumb "nav.dashboard", статусы "CANCELLED/FAILED/READY" +- Не все строки переведены + +--- + +## 11. ДУБЛИРОВАНИЕ ФУНКЦИОНАЛА + +| Функция | Дублируется в | Комментарий | +|---|---|---| +| Маппер колонок | Датасеты (breadcrumb) + Инструменты > Маппер колонок | Один и тот же функционал | +| Файловое хранилище | Инструменты > Файловое хранилище + Хранилище > Бэкапы | Один URL /tools/storage | +| Настройки | Шестерёнка ( /settings ) + Админ | Значительный overlap | +| Бэкапы | Инструменты > Бэкапы + Хранилище > Бэкапы | Разные URL, разный функционал | + +--- + +## 12. НЕ РАБОТАЕТ (КРИТИЧЕСКИЕ ПРОБЛЕМЫ) + +| Страница | Проблема | +|---|---| +| /datasets | Пустая страница, нет списка датасетов | +| /storage/repos | Зависает на "Загрузка..." | +| /migration/mappings | Кнопка disabled, нет данных | +| /translate/history | 0 успешных переводов из 400 записей (100% ошибок) | +| Dashboards Git | Ни один дашборд не привязан к репозиторию | + +--- + +## 13. РЕКОМЕНДАЦИИ + +### Критические (блокирующие): +1. **Починить датасеты:** /datasets пустая — вероятно, не подключен API или сломан эндпоинт +2. **Починить перевод:** 100% ошибок при переводе — проверить LLM провайдера и конфигурацию задачи "test" +3. **Починить /storage/repos:** страница зависает на "Загрузка..." +4. **Настроить Git:** ни один дашборд не имеет репозитория — без этого миграция не работает + +### Важные: +5. **Устранить дублирование навигации:** решить, где должен быть "Маппер колонок", "Файловое хранилище", "Настройки" +6. **Перевод интерфейса:** "nav.dashboard" и статусы CANCELLED/FAILED/READY должны быть локализованы +7. **Отчёты:** JSON не должен отображаться сырым — нужна читаемая визуализация +8. **AI Ассистент:** добавить приветствие / тултип / онбординг при первом открытии +9. **Миграция:** добавить второе окружение для тестирования миграции (dev→dev — бессмысленно) + +### Косметические: +10. **Опечатка:** "repositorys" → "repositories" +11. **Центр здоровья:** нет данных — либо скрыть, либо запустить валидацию +12. **Бэкапы:** даты создания показывают "--" — не хватает метаданных +13. **Удаление без подтверждения:** в файловом хранилище кнопка "Удалить" без confirm-диалога + +--- + +## Полная карта навигации + +``` +Superset Tools +├── РЕСУРСЫ +│ ├── Дашборды +│ │ ├── Обзор (/dashboards) +│ │ └── Центр здоровья (/dashboards/health) [пусто] +│ ├── Датасеты +│ │ ├── Все датасеты (/datasets) [пусто!] +│ │ └── Review Workspace (/datasets/review) +│ └── Миграция +│ ├── Обзор (/migration) [только dev] +│ └── Маппинги (/migration/mappings) [не работает] +├── ОПЕРАЦИИ +│ ├── Git (/git) [нет репозиториев] +│ ├── Переводы +│ │ ├── Задания (/translate) +│ │ ├── Словари (/translate/dictionaries) [пусто] +│ │ └── История (/translate/history) [100% ошибок] +│ └── Отчеты (/reports) [JSON сыром] +├── СИСТЕМА +│ ├── Инструменты +│ │ ├── Маппер колонок (/tools/mapper) +│ │ ├── Диагностика (/tools/debug) +│ │ ├── Файловое хранилище (/tools/storage) +│ │ └── Бэкапы (/tools/backups) +│ ├── Хранилище +│ │ ├── Бэкапы → /tools/storage [ДУБЛЬ] +│ │ └── Репозитории (/storage/repos) [зависает] +│ └── Админ +│ ├── Пользователи (/admin/users) +│ ├── Роли (/admin/roles) +│ ├── Настройка ADFS (/admin/settings) +│ └── Провайдеры LLM (/admin/settings/llm) +└── [ХЕДЕР] + ├── AI Ассистент (панель) + ├── Активность (панель задач) + ├── Настройки (/settings) [ДУБЛЬ с Админ] + └── Меню пользователя +```